baubs git
Commit acdf87b23769d7a45950689ca0e91aff3e6f0b7f
Author : Jan-Henrik Bruhn <jan-henrik.bruhn@offis.de>
Date : 2025-11-30T22:18:14+01:00
initial
Diff
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..a547bf3
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,24 @@
+# Logs
+logs
+*.log
+npm-debug.log*
+yarn-debug.log*
+yarn-error.log*
+pnpm-debug.log*
+lerna-debug.log*
+
+node_modules
+dist
+dist-ssr
+*.local
+
+# Editor directories and files
+.vscode/*
+!.vscode/extensions.json
+.idea
+.DS_Store
+*.suo
+*.ntvs*
+*.njsproj
+*.sln
+*.sw?
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..6aa1644
--- /dev/null
+++ b/README.md
@@ -0,0 +1,150 @@
+# Brother Embroidery Machine Web Controller
+
+A modern web application for controlling Brother embroidery machines via WebBluetooth.
+
+## Features
+
+- **WebBluetooth Connection**: Connect directly to Brother PP1 embroidery machines from your browser
+- **Pattern Upload**: Load and upload PEN format embroidery files
+- **Pattern Visualization**: Preview embroidery patterns on an interactive canvas with color information
+- **Real-time Monitoring**: Monitor sewing progress, position, and status in real-time
+- **Machine Control**: Start mask trace, start sewing, and manage patterns
+
+## Requirements
+
+- Modern browser with WebBluetooth support (Chrome, Edge, Opera)
+- HTTPS connection (required for WebBluetooth API)
+- Brother PP1 compatible embroidery machine with BLE
+
+## Getting Started
+
+### Installation
+
+```bash
+npm install
+```
+
+### Development
+
+```bash
+npm run dev
+```
+
+The application will be available at `http://localhost:5173`
+
+**Note**: WebBluetooth requires HTTPS. For local development, you can use:
+- `localhost` (works with HTTP)
+- A reverse proxy with SSL
+- Vite's HTTPS mode: `npm run dev -- --https`
+
+### Build for Production
+
+```bash
+npm run build
+```
+
+The built files will be in the `dist` directory.
+
+### Preview Production Build
+
+```bash
+npm run preview
+```
+
+## Usage
+
+1. **Connect to Machine**
+ - Click "Connect to Machine"
+ - Select your Brother embroidery machine from the browser's Bluetooth device picker
+ - Machine information and status will be displayed
+
+2. **Load Pattern**
+ - Click "Choose PEN File" and select a `.pen` embroidery file
+ - Pattern details and preview will be shown on the canvas
+ - Different colors are displayed in the preview
+
+3. **Upload to Machine**
+ - Click "Upload to Machine" to transfer the pattern
+ - Upload progress will be shown
+ - Pattern information will be retrieved from the machine
+
+4. **Start Mask Trace** (optional)
+ - Click "Start Mask Trace" to trace the pattern outline
+ - Confirm on the machine when prompted
+
+5. **Start Sewing**
+ - Click "Start Sewing" to begin the embroidery process
+ - Real-time progress, position, and status will be displayed
+ - Follow machine prompts for color changes
+
+6. **Monitor Progress**
+ - View current stitch count and completion percentage
+ - See real-time needle position
+ - Track elapsed time
+
+## Project Structure
+
+```
+web/
+├── src/
+│ ├── components/ # React components
+│ │ ├── MachineConnection.tsx
+│ │ ├── FileUpload.tsx
+│ │ ├── PatternCanvas.tsx
+│ │ └── ProgressMonitor.tsx
+│ ├── hooks/ # Custom React hooks
+│ │ └── useBrotherMachine.ts
+│ ├── services/ # BLE communication
+│ │ └── BrotherPP1Service.ts
+│ ├── types/ # TypeScript types
+│ │ └── machine.ts
+│ ├── utils/ # Utility functions
+│ │ └── penParser.ts
+│ ├── App.tsx # Main application component
+│ ├── App.css # Application styles
+│ └── main.tsx # Entry point
+├── public/ # Static assets
+├── package.json
+├── tsconfig.json
+└── vite.config.ts
+```
+
+## Technology Stack
+
+- **React 18**: UI framework
+- **TypeScript**: Type safety
+- **Vite**: Build tool and dev server
+- **WebBluetooth API**: BLE communication
+- **HTML5 Canvas**: Pattern visualization
+
+## Protocol
+
+The application implements the Brother PP1 BLE protocol:
+- Service UUID: `a76eb9e0-f3ac-4990-84cf-3a94d2426b2b`
+- Write Characteristic: `a76eb9e2-f3ac-4990-84cf-3a94d2426b2b`
+- Read Characteristic: `a76eb9e1-f3ac-4990-84cf-3a94d2426b2b`
+
+See `../emulator/PROTOCOL.md` for detailed protocol documentation.
+
+## PEN Format
+
+The application supports PEN format embroidery files:
+- Binary format with 4-byte stitch records
+- Coordinates in 0.1mm units
+- Supports multiple colors and color changes
+- Includes jump stitches and flags
+
+## Browser Compatibility
+
+WebBluetooth is supported in:
+- Chrome 56+
+- Edge 79+
+- Opera 43+
+
+**Not supported in:**
+- Firefox (no WebBluetooth support)
+- Safari (no WebBluetooth support)
+
+## License
+
+MIT
diff --git a/eslint.config.js b/eslint.config.js
new file mode 100644
index 0000000..5e6b472
--- /dev/null
+++ b/eslint.config.js
@@ -0,0 +1,23 @@
+import js from '@eslint/js'
+import globals from 'globals'
+import reactHooks from 'eslint-plugin-react-hooks'
+import reactRefresh from 'eslint-plugin-react-refresh'
+import tseslint from 'typescript-eslint'
+import { defineConfig, globalIgnores } from 'eslint/config'
+
+export default defineConfig([
+ globalIgnores(['dist']),
+ {
+ files: ['**/*.{ts,tsx}'],
+ extends: [
+ js.configs.recommended,
+ tseslint.configs.recommended,
+ reactHooks.configs.flat.recommended,
+ reactRefresh.configs.vite,
+ ],
+ languageOptions: {
+ ecmaVersion: 2020,
+ globals: globals.browser,
+ },
+ },
+])
diff --git a/index.html b/index.html
new file mode 100644
index 0000000..af88f03
--- /dev/null
+++ b/index.html
@@ -0,0 +1,13 @@
+<!doctype html>
+<html lang="en">
+ <head>
+ <meta charset="UTF-8" />
+ <link rel="icon" type="image/svg+xml" href="/vite.svg" />
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
+ <title>web</title>
+ </head>
+ <body>
+ <div id="root"></div>
+ <script type="module" src="/src/main.tsx"></script>
+ </body>
+</html>
diff --git a/package-lock.json b/package-lock.json
new file mode 100644
index 0000000..24b57bf
--- /dev/null
+++ b/package-lock.json
@@ -0,0 +1,3223 @@
+{
+ "name": "web",
+ "version": "0.0.0",
+ "lockfileVersion": 3,
+ "requires": true,
+ "packages": {
+ "": {
+ "name": "web",
+ "version": "0.0.0",
+ "dependencies": {
+ "pyodide": "^0.27.4",
+ "react": "^19.2.0",
+ "react-dom": "^19.2.0"
+ },
+ "devDependencies": {
+ "@eslint/js": "^9.39.1",
+ "@types/node": "^24.10.1",
+ "@types/react": "^19.2.5",
+ "@types/react-dom": "^19.2.3",
+ "@vitejs/plugin-react": "^5.1.1",
+ "eslint": "^9.39.1",
+ "eslint-plugin-react-hooks": "^7.0.1",
+ "eslint-plugin-react-refresh": "^0.4.24",
+ "globals": "^16.5.0",
+ "typescript": "~5.9.3",
+ "typescript-eslint": "^8.46.4",
+ "vite": "^7.2.4",
+ "vite-plugin-static-copy": "^3.1.4"
+ }
+ },
+ "node_modules/@babel/code-frame": {
+ "version": "7.27.1",
+ "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.27.1.tgz",
+ "integrity": "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==",
+ "dev": true,
+ "dependencies": {
+ "@babel/helper-validator-identifier": "^7.27.1",
+ "js-tokens": "^4.0.0",
+ "picocolors": "^1.1.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/compat-data": {
+ "version": "7.28.5",
+ "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.28.5.tgz",
+ "integrity": "sha512-6uFXyCayocRbqhZOB+6XcuZbkMNimwfVGFji8CTZnCzOHVGvDqzvitu1re2AU5LROliz7eQPhB8CpAMvnx9EjA==",
+ "dev": true,
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/core": {
+ "version": "7.28.5",
+ "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.28.5.tgz",
+ "integrity": "sha512-e7jT4DxYvIDLk1ZHmU/m/mB19rex9sv0c2ftBtjSBv+kVM/902eh0fINUzD7UwLLNR+jU585GxUJ8/EBfAM5fw==",
+ "dev": true,
+ "dependencies": {
+ "@babel/code-frame": "^7.27.1",
+ "@babel/generator": "^7.28.5",
+ "@babel/helper-compilation-targets": "^7.27.2",
+ "@babel/helper-module-transforms": "^7.28.3",
+ "@babel/helpers": "^7.28.4",
+ "@babel/parser": "^7.28.5",
+ "@babel/template": "^7.27.2",
+ "@babel/traverse": "^7.28.5",
+ "@babel/types": "^7.28.5",
+ "@jridgewell/remapping": "^2.3.5",
+ "convert-source-map": "^2.0.0",
+ "debug": "^4.1.0",
+ "gensync": "^1.0.0-beta.2",
+ "json5": "^2.2.3",
+ "semver": "^6.3.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/babel"
+ }
+ },
+ "node_modules/@babel/generator": {
+ "version": "7.28.5",
+ "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.28.5.tgz",
+ "integrity": "sha512-3EwLFhZ38J4VyIP6WNtt2kUdW9dokXA9Cr4IVIFHuCpZ3H8/YFOl5JjZHisrn1fATPBmKKqXzDFvh9fUwHz6CQ==",
+ "dev": true,
+ "dependencies": {
+ "@babel/parser": "^7.28.5",
+ "@babel/types": "^7.28.5",
+ "@jridgewell/gen-mapping": "^0.3.12",
+ "@jridgewell/trace-mapping": "^0.3.28",
+ "jsesc": "^3.0.2"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-compilation-targets": {
+ "version": "7.27.2",
+ "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.27.2.tgz",
+ "integrity": "sha512-2+1thGUUWWjLTYTHZWK1n8Yga0ijBz1XAhUXcKy81rd5g6yh7hGqMp45v7cadSbEHc9G3OTv45SyneRN3ps4DQ==",
+ "dev": true,
+ "dependencies": {
+ "@babel/compat-data": "^7.27.2",
+ "@babel/helper-validator-option": "^7.27.1",
+ "browserslist": "^4.24.0",
+ "lru-cache": "^5.1.1",
+ "semver": "^6.3.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-globals": {
+ "version": "7.28.0",
+ "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz",
+ "integrity": "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==",
+ "dev": true,
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-module-imports": {
+ "version": "7.27.1",
+ "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.27.1.tgz",
+ "integrity": "sha512-0gSFWUPNXNopqtIPQvlD5WgXYI5GY2kP2cCvoT8kczjbfcfuIljTbcWrulD1CIPIX2gt1wghbDy08yE1p+/r3w==",
+ "dev": true,
+ "dependencies": {
+ "@babel/traverse": "^7.27.1",
+ "@babel/types": "^7.27.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-module-transforms": {
+ "version": "7.28.3",
+ "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.28.3.tgz",
+ "integrity": "sha512-gytXUbs8k2sXS9PnQptz5o0QnpLL51SwASIORY6XaBKF88nsOT0Zw9szLqlSGQDP/4TljBAD5y98p2U1fqkdsw==",
+ "dev": true,
+ "dependencies": {
+ "@babel/helper-module-imports": "^7.27.1",
+ "@babel/helper-validator-identifier": "^7.27.1",
+ "@babel/traverse": "^7.28.3"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0"
+ }
+ },
+ "node_modules/@babel/helper-plugin-utils": {
+ "version": "7.27.1",
+ "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.27.1.tgz",
+ "integrity": "sha512-1gn1Up5YXka3YYAHGKpbideQ5Yjf1tDa9qYcgysz+cNCXukyLl6DjPXhD3VRwSb8c0J9tA4b2+rHEZtc6R0tlw==",
+ "dev": true,
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-string-parser": {
+ "version": "7.27.1",
+ "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz",
+ "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==",
+ "dev": true,
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-validator-identifier": {
+ "version": "7.28.5",
+ "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz",
+ "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==",
+ "dev": true,
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-validator-option": {
+ "version": "7.27.1",
+ "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz",
+ "integrity": "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==",
+ "dev": true,
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helpers": {
+ "version": "7.28.4",
+ "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.28.4.tgz",
+ "integrity": "sha512-HFN59MmQXGHVyYadKLVumYsA9dBFun/ldYxipEjzA4196jpLZd8UjEEBLkbEkvfYreDqJhZxYAWFPtrfhNpj4w==",
+ "dev": true,
+ "dependencies": {
+ "@babel/template": "^7.27.2",
+ "@babel/types": "^7.28.4"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/parser": {
+ "version": "7.28.5",
+ "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.28.5.tgz",
+ "integrity": "sha512-KKBU1VGYR7ORr3At5HAtUQ+TV3SzRCXmA/8OdDZiLDBIZxVyzXuztPjfLd3BV1PRAQGCMWWSHYhL0F8d5uHBDQ==",
+ "dev": true,
+ "dependencies": {
+ "@babel/types": "^7.28.5"
+ },
+ "bin": {
+ "parser": "bin/babel-parser.js"
+ },
+ "engines": {
+ "node": ">=6.0.0"
+ }
+ },
+ "node_modules/@babel/plugin-transform-react-jsx-self": {
+ "version": "7.27.1",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.27.1.tgz",
+ "integrity": "sha512-6UzkCs+ejGdZ5mFFC/OCUrv028ab2fp1znZmCZjAOBKiBK2jXD1O+BPSfX8X2qjJ75fZBMSnQn3Rq2mrBJK2mw==",
+ "dev": true,
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.27.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/plugin-transform-react-jsx-source": {
+ "version": "7.27.1",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.27.1.tgz",
+ "integrity": "sha512-zbwoTsBruTeKB9hSq73ha66iFeJHuaFkUbwvqElnygoNbj/jHRsSeokowZFN3CZ64IvEqcmmkVe89OPXc7ldAw==",
+ "dev": true,
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.27.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/template": {
+ "version": "7.27.2",
+ "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.27.2.tgz",
+ "integrity": "sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw==",
+ "dev": true,
+ "dependencies": {
+ "@babel/code-frame": "^7.27.1",
+ "@babel/parser": "^7.27.2",
+ "@babel/types": "^7.27.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/traverse": {
+ "version": "7.28.5",
+ "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.28.5.tgz",
+ "integrity": "sha512-TCCj4t55U90khlYkVV/0TfkJkAkUg3jZFA3Neb7unZT8CPok7iiRfaX0F+WnqWqt7OxhOn0uBKXCw4lbL8W0aQ==",
+ "dev": true,
+ "dependencies": {
+ "@babel/code-frame": "^7.27.1",
+ "@babel/generator": "^7.28.5",
+ "@babel/helper-globals": "^7.28.0",
+ "@babel/parser": "^7.28.5",
+ "@babel/template": "^7.27.2",
+ "@babel/types": "^7.28.5",
+ "debug": "^4.3.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/types": {
+ "version": "7.28.5",
+ "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.28.5.tgz",
+ "integrity": "sha512-qQ5m48eI/MFLQ5PxQj4PFaprjyCTLI37ElWMmNs0K8Lk3dVeOdNpB3ks8jc7yM5CDmVC73eMVk/trk3fgmrUpA==",
+ "dev": true,
+ "dependencies": {
+ "@babel/helper-string-parser": "^7.27.1",
+ "@babel/helper-validator-identifier": "^7.28.5"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@esbuild/aix-ppc64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.12.tgz",
+ "integrity": "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==",
+ "cpu": [
+ "ppc64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "aix"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/android-arm": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.12.tgz",
+ "integrity": "sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/android-arm64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.12.tgz",
+ "integrity": "sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/android-x64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.12.tgz",
+ "integrity": "sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/darwin-arm64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.12.tgz",
+ "integrity": "sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/darwin-x64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.12.tgz",
+ "integrity": "sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/freebsd-arm64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.12.tgz",
+ "integrity": "sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "freebsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/freebsd-x64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.12.tgz",
+ "integrity": "sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "freebsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-arm": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.12.tgz",
+ "integrity": "sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-arm64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.12.tgz",
+ "integrity": "sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-ia32": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.12.tgz",
+ "integrity": "sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA==",
+ "cpu": [
+ "ia32"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-loong64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.12.tgz",
+ "integrity": "sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng==",
+ "cpu": [
+ "loong64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-mips64el": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.12.tgz",
+ "integrity": "sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw==",
+ "cpu": [
+ "mips64el"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-ppc64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.12.tgz",
+ "integrity": "sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA==",
+ "cpu": [
+ "ppc64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-riscv64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.12.tgz",
+ "integrity": "sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w==",
+ "cpu": [
+ "riscv64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-s390x": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.12.tgz",
+ "integrity": "sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg==",
+ "cpu": [
+ "s390x"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-x64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.12.tgz",
+ "integrity": "sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/netbsd-arm64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.12.tgz",
+ "integrity": "sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "netbsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/netbsd-x64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.12.tgz",
+ "integrity": "sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "netbsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/openbsd-arm64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.12.tgz",
+ "integrity": "sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "openbsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/openbsd-x64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.12.tgz",
+ "integrity": "sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "openbsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/openharmony-arm64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.12.tgz",
+ "integrity": "sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "openharmony"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/sunos-x64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.12.tgz",
+ "integrity": "sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "sunos"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/win32-arm64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.12.tgz",
+ "integrity": "sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/win32-ia32": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.12.tgz",
+ "integrity": "sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ==",
+ "cpu": [
+ "ia32"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/win32-x64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.12.tgz",
+ "integrity": "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@eslint-community/eslint-utils": {
+ "version": "4.9.0",
+ "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.0.tgz",
+ "integrity": "sha512-ayVFHdtZ+hsq1t2Dy24wCmGXGe4q9Gu3smhLYALJrr473ZH27MsnSL+LKUlimp4BWJqMDMLmPpx/Q9R3OAlL4g==",
+ "dev": true,
+ "dependencies": {
+ "eslint-visitor-keys": "^3.4.3"
+ },
+ "engines": {
+ "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/eslint"
+ },
+ "peerDependencies": {
+ "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0"
+ }
+ },
+ "node_modules/@eslint-community/eslint-utils/node_modules/eslint-visitor-keys": {
+ "version": "3.4.3",
+ "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz",
+ "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==",
+ "dev": true,
+ "engines": {
+ "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/eslint"
+ }
+ },
+ "node_modules/@eslint-community/regexpp": {
+ "version": "4.12.2",
+ "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.2.tgz",
+ "integrity": "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==",
+ "dev": true,
+ "engines": {
+ "node": "^12.0.0 || ^14.0.0 || >=16.0.0"
+ }
+ },
+ "node_modules/@eslint/config-array": {
+ "version": "0.21.1",
+ "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.21.1.tgz",
+ "integrity": "sha512-aw1gNayWpdI/jSYVgzN5pL0cfzU02GT3NBpeT/DXbx1/1x7ZKxFPd9bwrzygx/qiwIQiJ1sw/zD8qY/kRvlGHA==",
+ "dev": true,
+ "dependencies": {
+ "@eslint/object-schema": "^2.1.7",
+ "debug": "^4.3.1",
+ "minimatch": "^3.1.2"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ }
+ },
+ "node_modules/@eslint/config-helpers": {
+ "version": "0.4.2",
+ "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.4.2.tgz",
+ "integrity": "sha512-gBrxN88gOIf3R7ja5K9slwNayVcZgK6SOUORm2uBzTeIEfeVaIhOpCtTox3P6R7o2jLFwLFTLnC7kU/RGcYEgw==",
+ "dev": true,
+ "dependencies": {
+ "@eslint/core": "^0.17.0"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ }
+ },
+ "node_modules/@eslint/core": {
+ "version": "0.17.0",
+ "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.17.0.tgz",
+ "integrity": "sha512-yL/sLrpmtDaFEiUj1osRP4TI2MDz1AddJL+jZ7KSqvBuliN4xqYY54IfdN8qD8Toa6g1iloph1fxQNkjOxrrpQ==",
+ "dev": true,
+ "dependencies": {
+ "@types/json-schema": "^7.0.15"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ }
+ },
+ "node_modules/@eslint/eslintrc": {
+ "version": "3.3.3",
+ "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.3.tgz",
+ "integrity": "sha512-Kr+LPIUVKz2qkx1HAMH8q1q6azbqBAsXJUxBl/ODDuVPX45Z9DfwB8tPjTi6nNZ8BuM3nbJxC5zCAg5elnBUTQ==",
+ "dev": true,
+ "dependencies": {
+ "ajv": "^6.12.4",
+ "debug": "^4.3.2",
+ "espree": "^10.0.1",
+ "globals": "^14.0.0",
+ "ignore": "^5.2.0",
+ "import-fresh": "^3.2.1",
+ "js-yaml": "^4.1.1",
+ "minimatch": "^3.1.2",
+ "strip-json-comments": "^3.1.1"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/eslint"
+ }
+ },
+ "node_modules/@eslint/eslintrc/node_modules/globals": {
+ "version": "14.0.0",
+ "resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz",
+ "integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==",
+ "dev": true,
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/@eslint/js": {
+ "version": "9.39.1",
+ "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.39.1.tgz",
+ "integrity": "sha512-S26Stp4zCy88tH94QbBv3XCuzRQiZ9yXofEILmglYTh/Ug/a9/umqvgFtYBAo3Lp0nsI/5/qH1CCrbdK3AP1Tw==",
+ "dev": true,
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "url": "https://eslint.org/donate"
+ }
+ },
+ "node_modules/@eslint/object-schema": {
+ "version": "2.1.7",
+ "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.7.tgz",
+ "integrity": "sha512-VtAOaymWVfZcmZbp6E2mympDIHvyjXs/12LqWYjVw6qjrfF+VK+fyG33kChz3nnK+SU5/NeHOqrTEHS8sXO3OA==",
+ "dev": true,
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ }
+ },
+ "node_modules/@eslint/plugin-kit": {
+ "version": "0.4.1",
+ "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.4.1.tgz",
+ "integrity": "sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA==",
+ "dev": true,
+ "dependencies": {
+ "@eslint/core": "^0.17.0",
+ "levn": "^0.4.1"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ }
+ },
+ "node_modules/@humanfs/core": {
+ "version": "0.19.1",
+ "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz",
+ "integrity": "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==",
+ "dev": true,
+ "engines": {
+ "node": ">=18.18.0"
+ }
+ },
+ "node_modules/@humanfs/node": {
+ "version": "0.16.7",
+ "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.7.tgz",
+ "integrity": "sha512-/zUx+yOsIrG4Y43Eh2peDeKCxlRt/gET6aHfaKpuq267qXdYDFViVHfMaLyygZOnl0kGWxFIgsBy8QFuTLUXEQ==",
+ "dev": true,
+ "dependencies": {
+ "@humanfs/core": "^0.19.1",
+ "@humanwhocodes/retry": "^0.4.0"
+ },
+ "engines": {
+ "node": ">=18.18.0"
+ }
+ },
+ "node_modules/@humanwhocodes/module-importer": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz",
+ "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==",
+ "dev": true,
+ "engines": {
+ "node": ">=12.22"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/nzakas"
+ }
+ },
+ "node_modules/@humanwhocodes/retry": {
+ "version": "0.4.3",
+ "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.3.tgz",
+ "integrity": "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==",
+ "dev": true,
+ "engines": {
+ "node": ">=18.18"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/nzakas"
+ }
+ },
+ "node_modules/@jridgewell/gen-mapping": {
+ "version": "0.3.13",
+ "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz",
+ "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==",
+ "dev": true,
+ "dependencies": {
+ "@jridgewell/sourcemap-codec": "^1.5.0",
+ "@jridgewell/trace-mapping": "^0.3.24"
+ }
+ },
+ "node_modules/@jridgewell/remapping": {
+ "version": "2.3.5",
+ "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz",
+ "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==",
+ "dev": true,
+ "dependencies": {
+ "@jridgewell/gen-mapping": "^0.3.5",
+ "@jridgewell/trace-mapping": "^0.3.24"
+ }
+ },
+ "node_modules/@jridgewell/resolve-uri": {
+ "version": "3.1.2",
+ "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz",
+ "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==",
+ "dev": true,
+ "engines": {
+ "node": ">=6.0.0"
+ }
+ },
+ "node_modules/@jridgewell/sourcemap-codec": {
+ "version": "1.5.5",
+ "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz",
+ "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==",
+ "dev": true
+ },
+ "node_modules/@jridgewell/trace-mapping": {
+ "version": "0.3.31",
+ "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz",
+ "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==",
+ "dev": true,
+ "dependencies": {
+ "@jridgewell/resolve-uri": "^3.1.0",
+ "@jridgewell/sourcemap-codec": "^1.4.14"
+ }
+ },
+ "node_modules/@rolldown/pluginutils": {
+ "version": "1.0.0-beta.47",
+ "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.47.tgz",
+ "integrity": "sha512-8QagwMH3kNCuzD8EWL8R2YPW5e4OrHNSAHRFDdmFqEwEaD/KcNKjVoumo+gP2vW5eKB2UPbM6vTYiGZX0ixLnw==",
+ "dev": true
+ },
+ "node_modules/@rollup/rollup-android-arm-eabi": {
+ "version": "4.53.3",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.53.3.tgz",
+ "integrity": "sha512-mRSi+4cBjrRLoaal2PnqH82Wqyb+d3HsPUN/W+WslCXsZsyHa9ZeQQX/pQsZaVIWDkPcpV6jJ+3KLbTbgnwv8w==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "android"
+ ]
+ },
+ "node_modules/@rollup/rollup-android-arm64": {
+ "version": "4.53.3",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.53.3.tgz",
+ "integrity": "sha512-CbDGaMpdE9sh7sCmTrTUyllhrg65t6SwhjlMJsLr+J8YjFuPmCEjbBSx4Z/e4SmDyH3aB5hGaJUP2ltV/vcs4w==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "android"
+ ]
+ },
+ "node_modules/@rollup/rollup-darwin-arm64": {
+ "version": "4.53.3",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.53.3.tgz",
+ "integrity": "sha512-Nr7SlQeqIBpOV6BHHGZgYBuSdanCXuw09hon14MGOLGmXAFYjx1wNvquVPmpZnl0tLjg25dEdr4IQ6GgyToCUA==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "darwin"
+ ]
+ },
+ "node_modules/@rollup/rollup-darwin-x64": {
+ "version": "4.53.3",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.53.3.tgz",
+ "integrity": "sha512-DZ8N4CSNfl965CmPktJ8oBnfYr3F8dTTNBQkRlffnUarJ2ohudQD17sZBa097J8xhQ26AwhHJ5mvUyQW8ddTsQ==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "darwin"
+ ]
+ },
+ "node_modules/@rollup/rollup-freebsd-arm64": {
+ "version": "4.53.3",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.53.3.tgz",
+ "integrity": "sha512-yMTrCrK92aGyi7GuDNtGn2sNW+Gdb4vErx4t3Gv/Tr+1zRb8ax4z8GWVRfr3Jw8zJWvpGHNpss3vVlbF58DZ4w==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "freebsd"
+ ]
+ },
+ "node_modules/@rollup/rollup-freebsd-x64": {
+ "version": "4.53.3",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.53.3.tgz",
+ "integrity": "sha512-lMfF8X7QhdQzseM6XaX0vbno2m3hlyZFhwcndRMw8fbAGUGL3WFMBdK0hbUBIUYcEcMhVLr1SIamDeuLBnXS+Q==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "freebsd"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-arm-gnueabihf": {
+ "version": "4.53.3",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.53.3.tgz",
+ "integrity": "sha512-k9oD15soC/Ln6d2Wv/JOFPzZXIAIFLp6B+i14KhxAfnq76ajt0EhYc5YPeX6W1xJkAdItcVT+JhKl1QZh44/qw==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-arm-musleabihf": {
+ "version": "4.53.3",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.53.3.tgz",
+ "integrity": "sha512-vTNlKq+N6CK/8UktsrFuc+/7NlEYVxgaEgRXVUVK258Z5ymho29skzW1sutgYjqNnquGwVUObAaxae8rZ6YMhg==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-arm64-gnu": {
+ "version": "4.53.3",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.53.3.tgz",
+ "integrity": "sha512-RGrFLWgMhSxRs/EWJMIFM1O5Mzuz3Xy3/mnxJp/5cVhZ2XoCAxJnmNsEyeMJtpK+wu0FJFWz+QF4mjCA7AUQ3w==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-arm64-musl": {
+ "version": "4.53.3",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.53.3.tgz",
+ "integrity": "sha512-kASyvfBEWYPEwe0Qv4nfu6pNkITLTb32p4yTgzFCocHnJLAHs+9LjUu9ONIhvfT/5lv4YS5muBHyuV84epBo/A==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-loong64-gnu": {
+ "version": "4.53.3",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.53.3.tgz",
+ "integrity": "sha512-JiuKcp2teLJwQ7vkJ95EwESWkNRFJD7TQgYmCnrPtlu50b4XvT5MOmurWNrCj3IFdyjBQ5p9vnrX4JM6I8OE7g==",
+ "cpu": [
+ "loong64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-ppc64-gnu": {
+ "version": "4.53.3",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.53.3.tgz",
+ "integrity": "sha512-EoGSa8nd6d3T7zLuqdojxC20oBfNT8nexBbB/rkxgKj5T5vhpAQKKnD+h3UkoMuTyXkP5jTjK/ccNRmQrPNDuw==",
+ "cpu": [
+ "ppc64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-riscv64-gnu": {
+ "version": "4.53.3",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.53.3.tgz",
+ "integrity": "sha512-4s+Wped2IHXHPnAEbIB0YWBv7SDohqxobiiPA1FIWZpX+w9o2i4LezzH/NkFUl8LRci/8udci6cLq+jJQlh+0g==",
+ "cpu": [
+ "riscv64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-riscv64-musl": {
+ "version": "4.53.3",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.53.3.tgz",
+ "integrity": "sha512-68k2g7+0vs2u9CxDt5ktXTngsxOQkSEV/xBbwlqYcUrAVh6P9EgMZvFsnHy4SEiUl46Xf0IObWVbMvPrr2gw8A==",
+ "cpu": [
+ "riscv64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-s390x-gnu": {
+ "version": "4.53.3",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.53.3.tgz",
+ "integrity": "sha512-VYsFMpULAz87ZW6BVYw3I6sWesGpsP9OPcyKe8ofdg9LHxSbRMd7zrVrr5xi/3kMZtpWL/wC+UIJWJYVX5uTKg==",
+ "cpu": [
+ "s390x"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-x64-gnu": {
+ "version": "4.53.3",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.53.3.tgz",
+ "integrity": "sha512-3EhFi1FU6YL8HTUJZ51imGJWEX//ajQPfqWLI3BQq4TlvHy4X0MOr5q3D2Zof/ka0d5FNdPwZXm3Yyib/UEd+w==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-x64-musl": {
+ "version": "4.53.3",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.53.3.tgz",
+ "integrity": "sha512-eoROhjcc6HbZCJr+tvVT8X4fW3/5g/WkGvvmwz/88sDtSJzO7r/blvoBDgISDiCjDRZmHpwud7h+6Q9JxFwq1Q==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-openharmony-arm64": {
+ "version": "4.53.3",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.53.3.tgz",
+ "integrity": "sha512-OueLAWgrNSPGAdUdIjSWXw+u/02BRTcnfw9PN41D2vq/JSEPnJnVuBgw18VkN8wcd4fjUs+jFHVM4t9+kBSNLw==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "openharmony"
+ ]
+ },
+ "node_modules/@rollup/rollup-win32-arm64-msvc": {
+ "version": "4.53.3",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.53.3.tgz",
+ "integrity": "sha512-GOFuKpsxR/whszbF/bzydebLiXIHSgsEUp6M0JI8dWvi+fFa1TD6YQa4aSZHtpmh2/uAlj/Dy+nmby3TJ3pkTw==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "win32"
+ ]
+ },
+ "node_modules/@rollup/rollup-win32-ia32-msvc": {
+ "version": "4.53.3",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.53.3.tgz",
+ "integrity": "sha512-iah+THLcBJdpfZ1TstDFbKNznlzoxa8fmnFYK4V67HvmuNYkVdAywJSoteUszvBQ9/HqN2+9AZghbajMsFT+oA==",
+ "cpu": [
+ "ia32"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "win32"
+ ]
+ },
+ "node_modules/@rollup/rollup-win32-x64-gnu": {
+ "version": "4.53.3",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.53.3.tgz",
+ "integrity": "sha512-J9QDiOIZlZLdcot5NXEepDkstocktoVjkaKUtqzgzpt2yWjGlbYiKyp05rWwk4nypbYUNoFAztEgixoLaSETkg==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "win32"
+ ]
+ },
+ "node_modules/@rollup/rollup-win32-x64-msvc": {
+ "version": "4.53.3",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.53.3.tgz",
+ "integrity": "sha512-UhTd8u31dXadv0MopwGgNOBpUVROFKWVQgAg5N1ESyCz8AuBcMqm4AuTjrwgQKGDfoFuz02EuMRHQIw/frmYKQ==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "win32"
+ ]
+ },
+ "node_modules/@types/babel__core": {
+ "version": "7.20.5",
+ "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz",
+ "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==",
+ "dev": true,
+ "dependencies": {
+ "@babel/parser": "^7.20.7",
+ "@babel/types": "^7.20.7",
+ "@types/babel__generator": "*",
+ "@types/babel__template": "*",
+ "@types/babel__traverse": "*"
+ }
+ },
+ "node_modules/@types/babel__generator": {
+ "version": "7.27.0",
+ "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.27.0.tgz",
+ "integrity": "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==",
+ "dev": true,
+ "dependencies": {
+ "@babel/types": "^7.0.0"
+ }
+ },
+ "node_modules/@types/babel__template": {
+ "version": "7.4.4",
+ "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz",
+ "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==",
+ "dev": true,
+ "dependencies": {
+ "@babel/parser": "^7.1.0",
+ "@babel/types": "^7.0.0"
+ }
+ },
+ "node_modules/@types/babel__traverse": {
+ "version": "7.28.0",
+ "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.28.0.tgz",
+ "integrity": "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==",
+ "dev": true,
+ "dependencies": {
+ "@babel/types": "^7.28.2"
+ }
+ },
+ "node_modules/@types/estree": {
+ "version": "1.0.8",
+ "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz",
+ "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==",
+ "dev": true
+ },
+ "node_modules/@types/json-schema": {
+ "version": "7.0.15",
+ "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz",
+ "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==",
+ "dev": true
+ },
+ "node_modules/@types/node": {
+ "version": "24.10.1",
+ "resolved": "https://registry.npmjs.org/@types/node/-/node-24.10.1.tgz",
+ "integrity": "sha512-GNWcUTRBgIRJD5zj+Tq0fKOJ5XZajIiBroOF0yvj2bSU1WvNdYS/dn9UxwsujGW4JX06dnHyjV2y9rRaybH0iQ==",
+ "dev": true,
+ "dependencies": {
+ "undici-types": "~7.16.0"
+ }
+ },
+ "node_modules/@types/react": {
+ "version": "19.2.7",
+ "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.7.tgz",
+ "integrity": "sha512-MWtvHrGZLFttgeEj28VXHxpmwYbor/ATPYbBfSFZEIRK0ecCFLl2Qo55z52Hss+UV9CRN7trSeq1zbgx7YDWWg==",
+ "dev": true,
+ "dependencies": {
+ "csstype": "^3.2.2"
+ }
+ },
+ "node_modules/@types/react-dom": {
+ "version": "19.2.3",
+ "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.2.3.tgz",
+ "integrity": "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==",
+ "dev": true,
+ "peerDependencies": {
+ "@types/react": "^19.2.0"
+ }
+ },
+ "node_modules/@typescript-eslint/eslint-plugin": {
+ "version": "8.48.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.48.0.tgz",
+ "integrity": "sha512-XxXP5tL1txl13YFtrECECQYeZjBZad4fyd3cFV4a19LkAY/bIp9fev3US4S5fDVV2JaYFiKAZ/GRTOLer+mbyQ==",
+ "dev": true,
+ "dependencies": {
+ "@eslint-community/regexpp": "^4.10.0",
+ "@typescript-eslint/scope-manager": "8.48.0",
+ "@typescript-eslint/type-utils": "8.48.0",
+ "@typescript-eslint/utils": "8.48.0",
+ "@typescript-eslint/visitor-keys": "8.48.0",
+ "graphemer": "^1.4.0",
+ "ignore": "^7.0.0",
+ "natural-compare": "^1.4.0",
+ "ts-api-utils": "^2.1.0"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ },
+ "peerDependencies": {
+ "@typescript-eslint/parser": "^8.48.0",
+ "eslint": "^8.57.0 || ^9.0.0",
+ "typescript": ">=4.8.4 <6.0.0"
+ }
+ },
+ "node_modules/@typescript-eslint/eslint-plugin/node_modules/ignore": {
+ "version": "7.0.5",
+ "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.5.tgz",
+ "integrity": "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==",
+ "dev": true,
+ "engines": {
+ "node": ">= 4"
+ }
+ },
+ "node_modules/@typescript-eslint/parser": {
+ "version": "8.48.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.48.0.tgz",
+ "integrity": "sha512-jCzKdm/QK0Kg4V4IK/oMlRZlY+QOcdjv89U2NgKHZk1CYTj82/RVSx1mV/0gqCVMJ/DA+Zf/S4NBWNF8GQ+eqQ==",
+ "dev": true,
+ "dependencies": {
+ "@typescript-eslint/scope-manager": "8.48.0",
+ "@typescript-eslint/types": "8.48.0",
+ "@typescript-eslint/typescript-estree": "8.48.0",
+ "@typescript-eslint/visitor-keys": "8.48.0",
+ "debug": "^4.3.4"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ },
+ "peerDependencies": {
+ "eslint": "^8.57.0 || ^9.0.0",
+ "typescript": ">=4.8.4 <6.0.0"
+ }
+ },
+ "node_modules/@typescript-eslint/project-service": {
+ "version": "8.48.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.48.0.tgz",
+ "integrity": "sha512-Ne4CTZyRh1BecBf84siv42wv5vQvVmgtk8AuiEffKTUo3DrBaGYZueJSxxBZ8fjk/N3DrgChH4TOdIOwOwiqqw==",
+ "dev": true,
+ "dependencies": {
+ "@typescript-eslint/tsconfig-utils": "^8.48.0",
+ "@typescript-eslint/types": "^8.48.0",
+ "debug": "^4.3.4"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ },
+ "peerDependencies": {
+ "typescript": ">=4.8.4 <6.0.0"
+ }
+ },
+ "node_modules/@typescript-eslint/scope-manager": {
+ "version": "8.48.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.48.0.tgz",
+ "integrity": "sha512-uGSSsbrtJrLduti0Q1Q9+BF1/iFKaxGoQwjWOIVNJv0o6omrdyR8ct37m4xIl5Zzpkp69Kkmvom7QFTtue89YQ==",
+ "dev": true,
+ "dependencies": {
+ "@typescript-eslint/types": "8.48.0",
+ "@typescript-eslint/visitor-keys": "8.48.0"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ }
+ },
+ "node_modules/@typescript-eslint/tsconfig-utils": {
+ "version": "8.48.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.48.0.tgz",
+ "integrity": "sha512-WNebjBdFdyu10sR1M4OXTt2OkMd5KWIL+LLfeH9KhgP+jzfDV/LI3eXzwJ1s9+Yc0Kzo2fQCdY/OpdusCMmh6w==",
+ "dev": true,
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ },
+ "peerDependencies": {
+ "typescript": ">=4.8.4 <6.0.0"
+ }
+ },
+ "node_modules/@typescript-eslint/type-utils": {
+ "version": "8.48.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.48.0.tgz",
+ "integrity": "sha512-zbeVaVqeXhhab6QNEKfK96Xyc7UQuoFWERhEnj3mLVnUWrQnv15cJNseUni7f3g557gm0e46LZ6IJ4NJVOgOpw==",
+ "dev": true,
+ "dependencies": {
+ "@typescript-eslint/types": "8.48.0",
+ "@typescript-eslint/typescript-estree": "8.48.0",
+ "@typescript-eslint/utils": "8.48.0",
+ "debug": "^4.3.4",
+ "ts-api-utils": "^2.1.0"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ },
+ "peerDependencies": {
+ "eslint": "^8.57.0 || ^9.0.0",
+ "typescript": ">=4.8.4 <6.0.0"
+ }
+ },
+ "node_modules/@typescript-eslint/types": {
+ "version": "8.48.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.48.0.tgz",
+ "integrity": "sha512-cQMcGQQH7kwKoVswD1xdOytxQR60MWKM1di26xSUtxehaDs/32Zpqsu5WJlXTtTTqyAVK8R7hvsUnIXRS+bjvA==",
+ "dev": true,
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ }
+ },
+ "node_modules/@typescript-eslint/typescript-estree": {
+ "version": "8.48.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.48.0.tgz",
+ "integrity": "sha512-ljHab1CSO4rGrQIAyizUS6UGHHCiAYhbfcIZ1zVJr5nMryxlXMVWS3duFPSKvSUbFPwkXMFk1k0EMIjub4sRRQ==",
+ "dev": true,
+ "dependencies": {
+ "@typescript-eslint/project-service": "8.48.0",
+ "@typescript-eslint/tsconfig-utils": "8.48.0",
+ "@typescript-eslint/types": "8.48.0",
+ "@typescript-eslint/visitor-keys": "8.48.0",
+ "debug": "^4.3.4",
+ "minimatch": "^9.0.4",
+ "semver": "^7.6.0",
+ "tinyglobby": "^0.2.15",
+ "ts-api-utils": "^2.1.0"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ },
+ "peerDependencies": {
+ "typescript": ">=4.8.4 <6.0.0"
+ }
+ },
+ "node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz",
+ "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==",
+ "dev": true,
+ "dependencies": {
+ "balanced-match": "^1.0.0"
+ }
+ },
+ "node_modules/@typescript-eslint/typescript-estree/node_modules/minimatch": {
+ "version": "9.0.5",
+ "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz",
+ "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==",
+ "dev": true,
+ "dependencies": {
+ "brace-expansion": "^2.0.1"
+ },
+ "engines": {
+ "node": ">=16 || 14 >=14.17"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ }
+ },
+ "node_modules/@typescript-eslint/typescript-estree/node_modules/semver": {
+ "version": "7.7.3",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz",
+ "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==",
+ "dev": true,
+ "bin": {
+ "semver": "bin/semver.js"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/@typescript-eslint/utils": {
+ "version": "8.48.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.48.0.tgz",
+ "integrity": "sha512-yTJO1XuGxCsSfIVt1+1UrLHtue8xz16V8apzPYI06W0HbEbEWHxHXgZaAgavIkoh+GeV6hKKd5jm0sS6OYxWXQ==",
+ "dev": true,
+ "dependencies": {
+ "@eslint-community/eslint-utils": "^4.7.0",
+ "@typescript-eslint/scope-manager": "8.48.0",
+ "@typescript-eslint/types": "8.48.0",
+ "@typescript-eslint/typescript-estree": "8.48.0"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ },
+ "peerDependencies": {
+ "eslint": "^8.57.0 || ^9.0.0",
+ "typescript": ">=4.8.4 <6.0.0"
+ }
+ },
+ "node_modules/@typescript-eslint/visitor-keys": {
+ "version": "8.48.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.48.0.tgz",
+ "integrity": "sha512-T0XJMaRPOH3+LBbAfzR2jalckP1MSG/L9eUtY0DEzUyVaXJ/t6zN0nR7co5kz0Jko/nkSYCBRkz1djvjajVTTg==",
+ "dev": true,
+ "dependencies": {
+ "@typescript-eslint/types": "8.48.0",
+ "eslint-visitor-keys": "^4.2.1"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ }
+ },
+ "node_modules/@vitejs/plugin-react": {
+ "version": "5.1.1",
+ "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-5.1.1.tgz",
+ "integrity": "sha512-WQfkSw0QbQ5aJ2CHYw23ZGkqnRwqKHD/KYsMeTkZzPT4Jcf0DcBxBtwMJxnu6E7oxw5+JC6ZAiePgh28uJ1HBA==",
+ "dev": true,
+ "dependencies": {
+ "@babel/core": "^7.28.5",
+ "@babel/plugin-transform-react-jsx-self": "^7.27.1",
+ "@babel/plugin-transform-react-jsx-source": "^7.27.1",
+ "@rolldown/pluginutils": "1.0.0-beta.47",
+ "@types/babel__core": "^7.20.5",
+ "react-refresh": "^0.18.0"
+ },
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ },
+ "peerDependencies": {
+ "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0"
+ }
+ },
+ "node_modules/acorn": {
+ "version": "8.15.0",
+ "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz",
+ "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==",
+ "dev": true,
+ "bin": {
+ "acorn": "bin/acorn"
+ },
+ "engines": {
+ "node": ">=0.4.0"
+ }
+ },
+ "node_modules/acorn-jsx": {
+ "version": "5.3.2",
+ "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz",
+ "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==",
+ "dev": true,
+ "peerDependencies": {
+ "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0"
+ }
+ },
+ "node_modules/ajv": {
+ "version": "6.12.6",
+ "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz",
+ "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==",
+ "dev": true,
+ "dependencies": {
+ "fast-deep-equal": "^3.1.1",
+ "fast-json-stable-stringify": "^2.0.0",
+ "json-schema-traverse": "^0.4.1",
+ "uri-js": "^4.2.2"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/epoberezkin"
+ }
+ },
+ "node_modules/ansi-styles": {
+ "version": "4.3.0",
+ "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
+ "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
+ "dev": true,
+ "dependencies": {
+ "color-convert": "^2.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/ansi-styles?sponsor=1"
+ }
+ },
+ "node_modules/anymatch": {
+ "version": "3.1.3",
+ "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz",
+ "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==",
+ "dev": true,
+ "dependencies": {
+ "normalize-path": "^3.0.0",
+ "picomatch": "^2.0.4"
+ },
+ "engines": {
+ "node": ">= 8"
+ }
+ },
+ "node_modules/anymatch/node_modules/picomatch": {
+ "version": "2.3.1",
+ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz",
+ "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==",
+ "dev": true,
+ "engines": {
+ "node": ">=8.6"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/jonschlinkert"
+ }
+ },
+ "node_modules/argparse": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz",
+ "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==",
+ "dev": true
+ },
+ "node_modules/balanced-match": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
+ "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
+ "dev": true
+ },
+ "node_modules/baseline-browser-mapping": {
+ "version": "2.8.32",
+ "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.8.32.tgz",
+ "integrity": "sha512-OPz5aBThlyLFgxyhdwf/s2+8ab3OvT7AdTNvKHBwpXomIYeXqpUUuT8LrdtxZSsWJ4R4CU1un4XGh5Ez3nlTpw==",
+ "dev": true,
+ "bin": {
+ "baseline-browser-mapping": "dist/cli.js"
+ }
+ },
+ "node_modules/binary-extensions": {
+ "version": "2.3.0",
+ "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz",
+ "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==",
+ "dev": true,
+ "engines": {
+ "node": ">=8"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/brace-expansion": {
+ "version": "1.1.12",
+ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz",
+ "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==",
+ "dev": true,
+ "dependencies": {
+ "balanced-match": "^1.0.0",
+ "concat-map": "0.0.1"
+ }
+ },
+ "node_modules/braces": {
+ "version": "3.0.3",
+ "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz",
+ "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==",
+ "dev": true,
+ "dependencies": {
+ "fill-range": "^7.1.1"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/browserslist": {
+ "version": "4.28.0",
+ "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.0.tgz",
+ "integrity": "sha512-tbydkR/CxfMwelN0vwdP/pLkDwyAASZ+VfWm4EOwlB6SWhx1sYnWLqo8N5j0rAzPfzfRaxt0mM/4wPU/Su84RQ==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/browserslist"
+ },
+ {
+ "type": "tidelift",
+ "url": "https://tidelift.com/funding/github/npm/browserslist"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "dependencies": {
+ "baseline-browser-mapping": "^2.8.25",
+ "caniuse-lite": "^1.0.30001754",
+ "electron-to-chromium": "^1.5.249",
+ "node-releases": "^2.0.27",
+ "update-browserslist-db": "^1.1.4"
+ },
+ "bin": {
+ "browserslist": "cli.js"
+ },
+ "engines": {
+ "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7"
+ }
+ },
+ "node_modules/callsites": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz",
+ "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==",
+ "dev": true,
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/caniuse-lite": {
+ "version": "1.0.30001757",
+ "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001757.tgz",
+ "integrity": "sha512-r0nnL/I28Zi/yjk1el6ilj27tKcdjLsNqAOZr0yVjWPrSQyHgKI2INaEWw21bAQSv2LXRt1XuCS/GomNpWOxsQ==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/browserslist"
+ },
+ {
+ "type": "tidelift",
+ "url": "https://tidelift.com/funding/github/npm/caniuse-lite"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ]
+ },
+ "node_modules/chalk": {
+ "version": "4.1.2",
+ "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
+ "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==",
+ "dev": true,
+ "dependencies": {
+ "ansi-styles": "^4.1.0",
+ "supports-color": "^7.1.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/chalk?sponsor=1"
+ }
+ },
+ "node_modules/chokidar": {
+ "version": "3.6.0",
+ "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz",
+ "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==",
+ "dev": true,
+ "dependencies": {
+ "anymatch": "~3.1.2",
+ "braces": "~3.0.2",
+ "glob-parent": "~5.1.2",
+ "is-binary-path": "~2.1.0",
+ "is-glob": "~4.0.1",
+ "normalize-path": "~3.0.0",
+ "readdirp": "~3.6.0"
+ },
+ "engines": {
+ "node": ">= 8.10.0"
+ },
+ "funding": {
+ "url": "https://paulmillr.com/funding/"
+ },
+ "optionalDependencies": {
+ "fsevents": "~2.3.2"
+ }
+ },
+ "node_modules/chokidar/node_modules/glob-parent": {
+ "version": "5.1.2",
+ "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz",
+ "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==",
+ "dev": true,
+ "dependencies": {
+ "is-glob": "^4.0.1"
+ },
+ "engines": {
+ "node": ">= 6"
+ }
+ },
+ "node_modules/color-convert": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
+ "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
+ "dev": true,
+ "dependencies": {
+ "color-name": "~1.1.4"
+ },
+ "engines": {
+ "node": ">=7.0.0"
+ }
+ },
+ "node_modules/color-name": {
+ "version": "1.1.4",
+ "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
+ "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
+ "dev": true
+ },
+ "node_modules/concat-map": {
+ "version": "0.0.1",
+ "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
+ "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==",
+ "dev": true
+ },
+ "node_modules/convert-source-map": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz",
+ "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==",
+ "dev": true
+ },
+ "node_modules/cross-spawn": {
+ "version": "7.0.6",
+ "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
+ "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==",
+ "dev": true,
+ "dependencies": {
+ "path-key": "^3.1.0",
+ "shebang-command": "^2.0.0",
+ "which": "^2.0.1"
+ },
+ "engines": {
+ "node": ">= 8"
+ }
+ },
+ "node_modules/csstype": {
+ "version": "3.2.3",
+ "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz",
+ "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==",
+ "dev": true
+ },
+ "node_modules/debug": {
+ "version": "4.4.3",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
+ "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==",
+ "dev": true,
+ "dependencies": {
+ "ms": "^2.1.3"
+ },
+ "engines": {
+ "node": ">=6.0"
+ },
+ "peerDependenciesMeta": {
+ "supports-color": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/deep-is": {
+ "version": "0.1.4",
+ "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz",
+ "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==",
+ "dev": true
+ },
+ "node_modules/electron-to-chromium": {
+ "version": "1.5.262",
+ "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.262.tgz",
+ "integrity": "sha512-NlAsMteRHek05jRUxUR0a5jpjYq9ykk6+kO0yRaMi5moe7u0fVIOeQ3Y30A8dIiWFBNUoQGi1ljb1i5VtS9WQQ==",
+ "dev": true
+ },
+ "node_modules/esbuild": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.12.tgz",
+ "integrity": "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==",
+ "dev": true,
+ "hasInstallScript": true,
+ "bin": {
+ "esbuild": "bin/esbuild"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "optionalDependencies": {
+ "@esbuild/aix-ppc64": "0.25.12",
+ "@esbuild/android-arm": "0.25.12",
+ "@esbuild/android-arm64": "0.25.12",
+ "@esbuild/android-x64": "0.25.12",
+ "@esbuild/darwin-arm64": "0.25.12",
+ "@esbuild/darwin-x64": "0.25.12",
+ "@esbuild/freebsd-arm64": "0.25.12",
+ "@esbuild/freebsd-x64": "0.25.12",
+ "@esbuild/linux-arm": "0.25.12",
+ "@esbuild/linux-arm64": "0.25.12",
+ "@esbuild/linux-ia32": "0.25.12",
+ "@esbuild/linux-loong64": "0.25.12",
+ "@esbuild/linux-mips64el": "0.25.12",
+ "@esbuild/linux-ppc64": "0.25.12",
+ "@esbuild/linux-riscv64": "0.25.12",
+ "@esbuild/linux-s390x": "0.25.12",
+ "@esbuild/linux-x64": "0.25.12",
+ "@esbuild/netbsd-arm64": "0.25.12",
+ "@esbuild/netbsd-x64": "0.25.12",
+ "@esbuild/openbsd-arm64": "0.25.12",
+ "@esbuild/openbsd-x64": "0.25.12",
+ "@esbuild/openharmony-arm64": "0.25.12",
+ "@esbuild/sunos-x64": "0.25.12",
+ "@esbuild/win32-arm64": "0.25.12",
+ "@esbuild/win32-ia32": "0.25.12",
+ "@esbuild/win32-x64": "0.25.12"
+ }
+ },
+ "node_modules/escalade": {
+ "version": "3.2.0",
+ "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz",
+ "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==",
+ "dev": true,
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/escape-string-regexp": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz",
+ "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==",
+ "dev": true,
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/eslint": {
+ "version": "9.39.1",
+ "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.39.1.tgz",
+ "integrity": "sha512-BhHmn2yNOFA9H9JmmIVKJmd288g9hrVRDkdoIgRCRuSySRUHH7r/DI6aAXW9T1WwUuY3DFgrcaqB+deURBLR5g==",
+ "dev": true,
+ "dependencies": {
+ "@eslint-community/eslint-utils": "^4.8.0",
+ "@eslint-community/regexpp": "^4.12.1",
+ "@eslint/config-array": "^0.21.1",
+ "@eslint/config-helpers": "^0.4.2",
+ "@eslint/core": "^0.17.0",
+ "@eslint/eslintrc": "^3.3.1",
+ "@eslint/js": "9.39.1",
+ "@eslint/plugin-kit": "^0.4.1",
+ "@humanfs/node": "^0.16.6",
+ "@humanwhocodes/module-importer": "^1.0.1",
+ "@humanwhocodes/retry": "^0.4.2",
+ "@types/estree": "^1.0.6",
+ "ajv": "^6.12.4",
+ "chalk": "^4.0.0",
+ "cross-spawn": "^7.0.6",
+ "debug": "^4.3.2",
+ "escape-string-regexp": "^4.0.0",
+ "eslint-scope": "^8.4.0",
+ "eslint-visitor-keys": "^4.2.1",
+ "espree": "^10.4.0",
+ "esquery": "^1.5.0",
+ "esutils": "^2.0.2",
+ "fast-deep-equal": "^3.1.3",
+ "file-entry-cache": "^8.0.0",
+ "find-up": "^5.0.0",
+ "glob-parent": "^6.0.2",
+ "ignore": "^5.2.0",
+ "imurmurhash": "^0.1.4",
+ "is-glob": "^4.0.0",
+ "json-stable-stringify-without-jsonify": "^1.0.1",
+ "lodash.merge": "^4.6.2",
+ "minimatch": "^3.1.2",
+ "natural-compare": "^1.4.0",
+ "optionator": "^0.9.3"
+ },
+ "bin": {
+ "eslint": "bin/eslint.js"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "url": "https://eslint.org/donate"
+ },
+ "peerDependencies": {
+ "jiti": "*"
+ },
+ "peerDependenciesMeta": {
+ "jiti": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/eslint-plugin-react-hooks": {
+ "version": "7.0.1",
+ "resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-7.0.1.tgz",
+ "integrity": "sha512-O0d0m04evaNzEPoSW+59Mezf8Qt0InfgGIBJnpC0h3NH/WjUAR7BIKUfysC6todmtiZ/A0oUVS8Gce0WhBrHsA==",
+ "dev": true,
+ "dependencies": {
+ "@babel/core": "^7.24.4",
+ "@babel/parser": "^7.24.4",
+ "hermes-parser": "^0.25.1",
+ "zod": "^3.25.0 || ^4.0.0",
+ "zod-validation-error": "^3.5.0 || ^4.0.0"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "peerDependencies": {
+ "eslint": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0"
+ }
+ },
+ "node_modules/eslint-plugin-react-refresh": {
+ "version": "0.4.24",
+ "resolved": "https://registry.npmjs.org/eslint-plugin-react-refresh/-/eslint-plugin-react-refresh-0.4.24.tgz",
+ "integrity": "sha512-nLHIW7TEq3aLrEYWpVaJ1dRgFR+wLDPN8e8FpYAql/bMV2oBEfC37K0gLEGgv9fy66juNShSMV8OkTqzltcG/w==",
+ "dev": true,
+ "peerDependencies": {
+ "eslint": ">=8.40"
+ }
+ },
+ "node_modules/eslint-scope": {
+ "version": "8.4.0",
+ "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.4.0.tgz",
+ "integrity": "sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==",
+ "dev": true,
+ "dependencies": {
+ "esrecurse": "^4.3.0",
+ "estraverse": "^5.2.0"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/eslint"
+ }
+ },
+ "node_modules/eslint-visitor-keys": {
+ "version": "4.2.1",
+ "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz",
+ "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==",
+ "dev": true,
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/eslint"
+ }
+ },
+ "node_modules/espree": {
+ "version": "10.4.0",
+ "resolved": "https://registry.npmjs.org/espree/-/espree-10.4.0.tgz",
+ "integrity": "sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==",
+ "dev": true,
+ "dependencies": {
+ "acorn": "^8.15.0",
+ "acorn-jsx": "^5.3.2",
+ "eslint-visitor-keys": "^4.2.1"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/eslint"
+ }
+ },
+ "node_modules/esquery": {
+ "version": "1.6.0",
+ "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.6.0.tgz",
+ "integrity": "sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg==",
+ "dev": true,
+ "dependencies": {
+ "estraverse": "^5.1.0"
+ },
+ "engines": {
+ "node": ">=0.10"
+ }
+ },
+ "node_modules/esrecurse": {
+ "version": "4.3.0",
+ "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz",
+ "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==",
+ "dev": true,
+ "dependencies": {
+ "estraverse": "^5.2.0"
+ },
+ "engines": {
+ "node": ">=4.0"
+ }
+ },
+ "node_modules/estraverse": {
+ "version": "5.3.0",
+ "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz",
+ "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==",
+ "dev": true,
+ "engines": {
+ "node": ">=4.0"
+ }
+ },
+ "node_modules/esutils": {
+ "version": "2.0.3",
+ "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz",
+ "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==",
+ "dev": true,
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/fast-deep-equal": {
+ "version": "3.1.3",
+ "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
+ "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==",
+ "dev": true
+ },
+ "node_modules/fast-json-stable-stringify": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz",
+ "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==",
+ "dev": true
+ },
+ "node_modules/fast-levenshtein": {
+ "version": "2.0.6",
+ "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz",
+ "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==",
+ "dev": true
+ },
+ "node_modules/fdir": {
+ "version": "6.5.0",
+ "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz",
+ "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==",
+ "dev": true,
+ "engines": {
+ "node": ">=12.0.0"
+ },
+ "peerDependencies": {
+ "picomatch": "^3 || ^4"
+ },
+ "peerDependenciesMeta": {
+ "picomatch": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/file-entry-cache": {
+ "version": "8.0.0",
+ "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz",
+ "integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==",
+ "dev": true,
+ "dependencies": {
+ "flat-cache": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=16.0.0"
+ }
+ },
+ "node_modules/fill-range": {
+ "version": "7.1.1",
+ "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz",
+ "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==",
+ "dev": true,
+ "dependencies": {
+ "to-regex-range": "^5.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/find-up": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz",
+ "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==",
+ "dev": true,
+ "dependencies": {
+ "locate-path": "^6.0.0",
+ "path-exists": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/flat-cache": {
+ "version": "4.0.1",
+ "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz",
+ "integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==",
+ "dev": true,
+ "dependencies": {
+ "flatted": "^3.2.9",
+ "keyv": "^4.5.4"
+ },
+ "engines": {
+ "node": ">=16"
+ }
+ },
+ "node_modules/flatted": {
+ "version": "3.3.3",
+ "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.3.tgz",
+ "integrity": "sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==",
+ "dev": true
+ },
+ "node_modules/fsevents": {
+ "version": "2.3.3",
+ "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
+ "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
+ "dev": true,
+ "hasInstallScript": true,
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": "^8.16.0 || ^10.6.0 || >=11.0.0"
+ }
+ },
+ "node_modules/gensync": {
+ "version": "1.0.0-beta.2",
+ "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz",
+ "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==",
+ "dev": true,
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/glob-parent": {
+ "version": "6.0.2",
+ "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz",
+ "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==",
+ "dev": true,
+ "dependencies": {
+ "is-glob": "^4.0.3"
+ },
+ "engines": {
+ "node": ">=10.13.0"
+ }
+ },
+ "node_modules/globals": {
+ "version": "16.5.0",
+ "resolved": "https://registry.npmjs.org/globals/-/globals-16.5.0.tgz",
+ "integrity": "sha512-c/c15i26VrJ4IRt5Z89DnIzCGDn9EcebibhAOjw5ibqEHsE1wLUgkPn9RDmNcUKyU87GeaL633nyJ+pplFR2ZQ==",
+ "dev": true,
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/graphemer": {
+ "version": "1.4.0",
+ "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz",
+ "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==",
+ "dev": true
+ },
+ "node_modules/has-flag": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
+ "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==",
+ "dev": true,
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/hermes-estree": {
+ "version": "0.25.1",
+ "resolved": "https://registry.npmjs.org/hermes-estree/-/hermes-estree-0.25.1.tgz",
+ "integrity": "sha512-0wUoCcLp+5Ev5pDW2OriHC2MJCbwLwuRx+gAqMTOkGKJJiBCLjtrvy4PWUGn6MIVefecRpzoOZ/UV6iGdOr+Cw==",
+ "dev": true
+ },
+ "node_modules/hermes-parser": {
+ "version": "0.25.1",
+ "resolved": "https://registry.npmjs.org/hermes-parser/-/hermes-parser-0.25.1.tgz",
+ "integrity": "sha512-6pEjquH3rqaI6cYAXYPcz9MS4rY6R4ngRgrgfDshRptUZIc3lw0MCIJIGDj9++mfySOuPTHB4nrSW99BCvOPIA==",
+ "dev": true,
+ "dependencies": {
+ "hermes-estree": "0.25.1"
+ }
+ },
+ "node_modules/ignore": {
+ "version": "5.3.2",
+ "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz",
+ "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==",
+ "dev": true,
+ "engines": {
+ "node": ">= 4"
+ }
+ },
+ "node_modules/import-fresh": {
+ "version": "3.3.1",
+ "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz",
+ "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==",
+ "dev": true,
+ "dependencies": {
+ "parent-module": "^1.0.0",
+ "resolve-from": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=6"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/imurmurhash": {
+ "version": "0.1.4",
+ "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz",
+ "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==",
+ "dev": true,
+ "engines": {
+ "node": ">=0.8.19"
+ }
+ },
+ "node_modules/is-binary-path": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz",
+ "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==",
+ "dev": true,
+ "dependencies": {
+ "binary-extensions": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/is-extglob": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz",
+ "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==",
+ "dev": true,
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/is-glob": {
+ "version": "4.0.3",
+ "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz",
+ "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==",
+ "dev": true,
+ "dependencies": {
+ "is-extglob": "^2.1.1"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/is-number": {
+ "version": "7.0.0",
+ "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz",
+ "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==",
+ "dev": true,
+ "engines": {
+ "node": ">=0.12.0"
+ }
+ },
+ "node_modules/isexe": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
+ "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==",
+ "dev": true
+ },
+ "node_modules/js-tokens": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
+ "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==",
+ "dev": true
+ },
+ "node_modules/js-yaml": {
+ "version": "4.1.1",
+ "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz",
+ "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==",
+ "dev": true,
+ "dependencies": {
+ "argparse": "^2.0.1"
+ },
+ "bin": {
+ "js-yaml": "bin/js-yaml.js"
+ }
+ },
+ "node_modules/jsesc": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz",
+ "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==",
+ "dev": true,
+ "bin": {
+ "jsesc": "bin/jsesc"
+ },
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/json-buffer": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz",
+ "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==",
+ "dev": true
+ },
+ "node_modules/json-schema-traverse": {
+ "version": "0.4.1",
+ "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz",
+ "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==",
+ "dev": true
+ },
+ "node_modules/json-stable-stringify-without-jsonify": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz",
+ "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==",
+ "dev": true
+ },
+ "node_modules/json5": {
+ "version": "2.2.3",
+ "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz",
+ "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==",
+ "dev": true,
+ "bin": {
+ "json5": "lib/cli.js"
+ },
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/keyv": {
+ "version": "4.5.4",
+ "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz",
+ "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==",
+ "dev": true,
+ "dependencies": {
+ "json-buffer": "3.0.1"
+ }
+ },
+ "node_modules/levn": {
+ "version": "0.4.1",
+ "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz",
+ "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==",
+ "dev": true,
+ "dependencies": {
+ "prelude-ls": "^1.2.1",
+ "type-check": "~0.4.0"
+ },
+ "engines": {
+ "node": ">= 0.8.0"
+ }
+ },
+ "node_modules/locate-path": {
+ "version": "6.0.0",
+ "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz",
+ "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==",
+ "dev": true,
+ "dependencies": {
+ "p-locate": "^5.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/lodash.merge": {
+ "version": "4.6.2",
+ "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz",
+ "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==",
+ "dev": true
+ },
+ "node_modules/lru-cache": {
+ "version": "5.1.1",
+ "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz",
+ "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==",
+ "dev": true,
+ "dependencies": {
+ "yallist": "^3.0.2"
+ }
+ },
+ "node_modules/minimatch": {
+ "version": "3.1.2",
+ "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
+ "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
+ "dev": true,
+ "dependencies": {
+ "brace-expansion": "^1.1.7"
+ },
+ "engines": {
+ "node": "*"
+ }
+ },
+ "node_modules/ms": {
+ "version": "2.1.3",
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
+ "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
+ "dev": true
+ },
+ "node_modules/nanoid": {
+ "version": "3.3.11",
+ "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz",
+ "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "bin": {
+ "nanoid": "bin/nanoid.cjs"
+ },
+ "engines": {
+ "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
+ }
+ },
+ "node_modules/natural-compare": {
+ "version": "1.4.0",
+ "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz",
+ "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==",
+ "dev": true
+ },
+ "node_modules/node-releases": {
+ "version": "2.0.27",
+ "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.27.tgz",
+ "integrity": "sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA==",
+ "dev": true
+ },
+ "node_modules/normalize-path": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz",
+ "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==",
+ "dev": true,
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/optionator": {
+ "version": "0.9.4",
+ "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz",
+ "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==",
+ "dev": true,
+ "dependencies": {
+ "deep-is": "^0.1.3",
+ "fast-levenshtein": "^2.0.6",
+ "levn": "^0.4.1",
+ "prelude-ls": "^1.2.1",
+ "type-check": "^0.4.0",
+ "word-wrap": "^1.2.5"
+ },
+ "engines": {
+ "node": ">= 0.8.0"
+ }
+ },
+ "node_modules/p-limit": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz",
+ "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==",
+ "dev": true,
+ "dependencies": {
+ "yocto-queue": "^0.1.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/p-locate": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz",
+ "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==",
+ "dev": true,
+ "dependencies": {
+ "p-limit": "^3.0.2"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/p-map": {
+ "version": "7.0.4",
+ "resolved": "https://registry.npmjs.org/p-map/-/p-map-7.0.4.tgz",
+ "integrity": "sha512-tkAQEw8ysMzmkhgw8k+1U/iPhWNhykKnSk4Rd5zLoPJCuJaGRPo6YposrZgaxHKzDHdDWWZvE/Sk7hsL2X/CpQ==",
+ "dev": true,
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/parent-module": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz",
+ "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==",
+ "dev": true,
+ "dependencies": {
+ "callsites": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/path-exists": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz",
+ "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==",
+ "dev": true,
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/path-key": {
+ "version": "3.1.1",
+ "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz",
+ "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==",
+ "dev": true,
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/picocolors": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
+ "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==",
+ "dev": true
+ },
+ "node_modules/picomatch": {
+ "version": "4.0.3",
+ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz",
+ "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
+ "dev": true,
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/jonschlinkert"
+ }
+ },
+ "node_modules/postcss": {
+ "version": "8.5.6",
+ "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz",
+ "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/postcss/"
+ },
+ {
+ "type": "tidelift",
+ "url": "https://tidelift.com/funding/github/npm/postcss"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "dependencies": {
+ "nanoid": "^3.3.11",
+ "picocolors": "^1.1.1",
+ "source-map-js": "^1.2.1"
+ },
+ "engines": {
+ "node": "^10 || ^12 || >=14"
+ }
+ },
+ "node_modules/prelude-ls": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz",
+ "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==",
+ "dev": true,
+ "engines": {
+ "node": ">= 0.8.0"
+ }
+ },
+ "node_modules/punycode": {
+ "version": "2.3.1",
+ "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz",
+ "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==",
+ "dev": true,
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/pyodide": {
+ "version": "0.27.7",
+ "resolved": "https://registry.npmjs.org/pyodide/-/pyodide-0.27.7.tgz",
+ "integrity": "sha512-RUSVJlhQdfWfgO9hVHCiXoG+nVZQRS5D9FzgpLJ/VcgGBLSAKoPL8kTiOikxbHQm1kRISeWUBdulEgO26qpSRA==",
+ "dependencies": {
+ "ws": "^8.5.0"
+ },
+ "engines": {
+ "node": ">=18.0.0"
+ }
+ },
+ "node_modules/react": {
+ "version": "19.2.0",
+ "resolved": "https://registry.npmjs.org/react/-/react-19.2.0.tgz",
+ "integrity": "sha512-tmbWg6W31tQLeB5cdIBOicJDJRR2KzXsV7uSK9iNfLWQ5bIZfxuPEHp7M8wiHyHnn0DD1i7w3Zmin0FtkrwoCQ==",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/react-dom": {
+ "version": "19.2.0",
+ "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.0.tgz",
+ "integrity": "sha512-UlbRu4cAiGaIewkPyiRGJk0imDN2T3JjieT6spoL2UeSf5od4n5LB/mQ4ejmxhCFT1tYe8IvaFulzynWovsEFQ==",
+ "dependencies": {
+ "scheduler": "^0.27.0"
+ },
+ "peerDependencies": {
+ "react": "^19.2.0"
+ }
+ },
+ "node_modules/react-refresh": {
+ "version": "0.18.0",
+ "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.18.0.tgz",
+ "integrity": "sha512-QgT5//D3jfjJb6Gsjxv0Slpj23ip+HtOpnNgnb2S5zU3CB26G/IDPGoy4RJB42wzFE46DRsstbW6tKHoKbhAxw==",
+ "dev": true,
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/readdirp": {
+ "version": "3.6.0",
+ "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz",
+ "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==",
+ "dev": true,
+ "dependencies": {
+ "picomatch": "^2.2.1"
+ },
+ "engines": {
+ "node": ">=8.10.0"
+ }
+ },
+ "node_modules/readdirp/node_modules/picomatch": {
+ "version": "2.3.1",
+ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz",
+ "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==",
+ "dev": true,
+ "engines": {
+ "node": ">=8.6"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/jonschlinkert"
+ }
+ },
+ "node_modules/resolve-from": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz",
+ "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==",
+ "dev": true,
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/rollup": {
+ "version": "4.53.3",
+ "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.53.3.tgz",
+ "integrity": "sha512-w8GmOxZfBmKknvdXU1sdM9NHcoQejwF/4mNgj2JuEEdRaHwwF12K7e9eXn1nLZ07ad+du76mkVsyeb2rKGllsA==",
+ "dev": true,
+ "dependencies": {
+ "@types/estree": "1.0.8"
+ },
+ "bin": {
+ "rollup": "dist/bin/rollup"
+ },
+ "engines": {
+ "node": ">=18.0.0",
+ "npm": ">=8.0.0"
+ },
+ "optionalDependencies": {
+ "@rollup/rollup-android-arm-eabi": "4.53.3",
+ "@rollup/rollup-android-arm64": "4.53.3",
+ "@rollup/rollup-darwin-arm64": "4.53.3",
+ "@rollup/rollup-darwin-x64": "4.53.3",
+ "@rollup/rollup-freebsd-arm64": "4.53.3",
+ "@rollup/rollup-freebsd-x64": "4.53.3",
+ "@rollup/rollup-linux-arm-gnueabihf": "4.53.3",
+ "@rollup/rollup-linux-arm-musleabihf": "4.53.3",
+ "@rollup/rollup-linux-arm64-gnu": "4.53.3",
+ "@rollup/rollup-linux-arm64-musl": "4.53.3",
+ "@rollup/rollup-linux-loong64-gnu": "4.53.3",
+ "@rollup/rollup-linux-ppc64-gnu": "4.53.3",
+ "@rollup/rollup-linux-riscv64-gnu": "4.53.3",
+ "@rollup/rollup-linux-riscv64-musl": "4.53.3",
+ "@rollup/rollup-linux-s390x-gnu": "4.53.3",
+ "@rollup/rollup-linux-x64-gnu": "4.53.3",
+ "@rollup/rollup-linux-x64-musl": "4.53.3",
+ "@rollup/rollup-openharmony-arm64": "4.53.3",
+ "@rollup/rollup-win32-arm64-msvc": "4.53.3",
+ "@rollup/rollup-win32-ia32-msvc": "4.53.3",
+ "@rollup/rollup-win32-x64-gnu": "4.53.3",
+ "@rollup/rollup-win32-x64-msvc": "4.53.3",
+ "fsevents": "~2.3.2"
+ }
+ },
+ "node_modules/scheduler": {
+ "version": "0.27.0",
+ "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz",
+ "integrity": "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q=="
+ },
+ "node_modules/semver": {
+ "version": "6.3.1",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz",
+ "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==",
+ "dev": true,
+ "bin": {
+ "semver": "bin/semver.js"
+ }
+ },
+ "node_modules/shebang-command": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
+ "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==",
+ "dev": true,
+ "dependencies": {
+ "shebang-regex": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/shebang-regex": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz",
+ "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==",
+ "dev": true,
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/source-map-js": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
+ "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==",
+ "dev": true,
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/strip-json-comments": {
+ "version": "3.1.1",
+ "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz",
+ "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==",
+ "dev": true,
+ "engines": {
+ "node": ">=8"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/supports-color": {
+ "version": "7.2.0",
+ "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
+ "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==",
+ "dev": true,
+ "dependencies": {
+ "has-flag": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/tinyglobby": {
+ "version": "0.2.15",
+ "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz",
+ "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==",
+ "dev": true,
+ "dependencies": {
+ "fdir": "^6.5.0",
+ "picomatch": "^4.0.3"
+ },
+ "engines": {
+ "node": ">=12.0.0"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/SuperchupuDev"
+ }
+ },
+ "node_modules/to-regex-range": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz",
+ "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==",
+ "dev": true,
+ "dependencies": {
+ "is-number": "^7.0.0"
+ },
+ "engines": {
+ "node": ">=8.0"
+ }
+ },
+ "node_modules/ts-api-utils": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.1.0.tgz",
+ "integrity": "sha512-CUgTZL1irw8u29bzrOD/nH85jqyc74D6SshFgujOIA7osm2Rz7dYH77agkx7H4FBNxDq7Cjf+IjaX/8zwFW+ZQ==",
+ "dev": true,
+ "engines": {
+ "node": ">=18.12"
+ },
+ "peerDependencies": {
+ "typescript": ">=4.8.4"
+ }
+ },
+ "node_modules/type-check": {
+ "version": "0.4.0",
+ "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz",
+ "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==",
+ "dev": true,
+ "dependencies": {
+ "prelude-ls": "^1.2.1"
+ },
+ "engines": {
+ "node": ">= 0.8.0"
+ }
+ },
+ "node_modules/typescript": {
+ "version": "5.9.3",
+ "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
+ "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
+ "dev": true,
+ "bin": {
+ "tsc": "bin/tsc",
+ "tsserver": "bin/tsserver"
+ },
+ "engines": {
+ "node": ">=14.17"
+ }
+ },
+ "node_modules/typescript-eslint": {
+ "version": "8.48.0",
+ "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.48.0.tgz",
+ "integrity": "sha512-fcKOvQD9GUn3Xw63EgiDqhvWJ5jsyZUaekl3KVpGsDJnN46WJTe3jWxtQP9lMZm1LJNkFLlTaWAxK2vUQR+cqw==",
+ "dev": true,
+ "dependencies": {
+ "@typescript-eslint/eslint-plugin": "8.48.0",
+ "@typescript-eslint/parser": "8.48.0",
+ "@typescript-eslint/typescript-estree": "8.48.0",
+ "@typescript-eslint/utils": "8.48.0"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ },
+ "peerDependencies": {
+ "eslint": "^8.57.0 || ^9.0.0",
+ "typescript": ">=4.8.4 <6.0.0"
+ }
+ },
+ "node_modules/undici-types": {
+ "version": "7.16.0",
+ "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.16.0.tgz",
+ "integrity": "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==",
+ "dev": true
+ },
+ "node_modules/update-browserslist-db": {
+ "version": "1.1.4",
+ "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.4.tgz",
+ "integrity": "sha512-q0SPT4xyU84saUX+tomz1WLkxUbuaJnR1xWt17M7fJtEJigJeWUNGUqrauFXsHnqev9y9JTRGwk13tFBuKby4A==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/browserslist"
+ },
+ {
+ "type": "tidelift",
+ "url": "https://tidelift.com/funding/github/npm/browserslist"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "dependencies": {
+ "escalade": "^3.2.0",
+ "picocolors": "^1.1.1"
+ },
+ "bin": {
+ "update-browserslist-db": "cli.js"
+ },
+ "peerDependencies": {
+ "browserslist": ">= 4.21.0"
+ }
+ },
+ "node_modules/uri-js": {
+ "version": "4.4.1",
+ "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz",
+ "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==",
+ "dev": true,
+ "dependencies": {
+ "punycode": "^2.1.0"
+ }
+ },
+ "node_modules/vite": {
+ "version": "7.2.4",
+ "resolved": "https://registry.npmjs.org/vite/-/vite-7.2.4.tgz",
+ "integrity": "sha512-NL8jTlbo0Tn4dUEXEsUg8KeyG/Lkmc4Fnzb8JXN/Ykm9G4HNImjtABMJgkQoVjOBN/j2WAwDTRytdqJbZsah7w==",
+ "dev": true,
+ "dependencies": {
+ "esbuild": "^0.25.0",
+ "fdir": "^6.5.0",
+ "picomatch": "^4.0.3",
+ "postcss": "^8.5.6",
+ "rollup": "^4.43.0",
+ "tinyglobby": "^0.2.15"
+ },
+ "bin": {
+ "vite": "bin/vite.js"
+ },
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ },
+ "funding": {
+ "url": "https://github.com/vitejs/vite?sponsor=1"
+ },
+ "optionalDependencies": {
+ "fsevents": "~2.3.3"
+ },
+ "peerDependencies": {
+ "@types/node": "^20.19.0 || >=22.12.0",
+ "jiti": ">=1.21.0",
+ "less": "^4.0.0",
+ "lightningcss": "^1.21.0",
+ "sass": "^1.70.0",
+ "sass-embedded": "^1.70.0",
+ "stylus": ">=0.54.8",
+ "sugarss": "^5.0.0",
+ "terser": "^5.16.0",
+ "tsx": "^4.8.1",
+ "yaml": "^2.4.2"
+ },
+ "peerDependenciesMeta": {
+ "@types/node": {
+ "optional": true
+ },
+ "jiti": {
+ "optional": true
+ },
+ "less": {
+ "optional": true
+ },
+ "lightningcss": {
+ "optional": true
+ },
+ "sass": {
+ "optional": true
+ },
+ "sass-embedded": {
+ "optional": true
+ },
+ "stylus": {
+ "optional": true
+ },
+ "sugarss": {
+ "optional": true
+ },
+ "terser": {
+ "optional": true
+ },
+ "tsx": {
+ "optional": true
+ },
+ "yaml": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/vite-plugin-static-copy": {
+ "version": "3.1.4",
+ "resolved": "https://registry.npmjs.org/vite-plugin-static-copy/-/vite-plugin-static-copy-3.1.4.tgz",
+ "integrity": "sha512-iCmr4GSw4eSnaB+G8zc2f4dxSuDjbkjwpuBLLGvQYR9IW7rnDzftnUjOH5p4RYR+d4GsiBqXRvzuFhs5bnzVyw==",
+ "dev": true,
+ "dependencies": {
+ "chokidar": "^3.6.0",
+ "p-map": "^7.0.3",
+ "picocolors": "^1.1.1",
+ "tinyglobby": "^0.2.15"
+ },
+ "engines": {
+ "node": "^18.0.0 || >=20.0.0"
+ },
+ "peerDependencies": {
+ "vite": "^5.0.0 || ^6.0.0 || ^7.0.0"
+ }
+ },
+ "node_modules/which": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
+ "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==",
+ "dev": true,
+ "dependencies": {
+ "isexe": "^2.0.0"
+ },
+ "bin": {
+ "node-which": "bin/node-which"
+ },
+ "engines": {
+ "node": ">= 8"
+ }
+ },
+ "node_modules/word-wrap": {
+ "version": "1.2.5",
+ "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz",
+ "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==",
+ "dev": true,
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/ws": {
+ "version": "8.18.3",
+ "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.3.tgz",
+ "integrity": "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg==",
+ "engines": {
+ "node": ">=10.0.0"
+ },
+ "peerDependencies": {
+ "bufferutil": "^4.0.1",
+ "utf-8-validate": ">=5.0.2"
+ },
+ "peerDependenciesMeta": {
+ "bufferutil": {
+ "optional": true
+ },
+ "utf-8-validate": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/yallist": {
+ "version": "3.1.1",
+ "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz",
+ "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==",
+ "dev": true
+ },
+ "node_modules/yocto-queue": {
+ "version": "0.1.0",
+ "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz",
+ "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==",
+ "dev": true,
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/zod": {
+ "version": "4.1.13",
+ "resolved": "https://registry.npmjs.org/zod/-/zod-4.1.13.tgz",
+ "integrity": "sha512-AvvthqfqrAhNH9dnfmrfKzX5upOdjUVJYFqNSlkmGf64gRaTzlPwz99IHYnVs28qYAybvAlBV+H7pn0saFY4Ig==",
+ "dev": true,
+ "funding": {
+ "url": "https://github.com/sponsors/colinhacks"
+ }
+ },
+ "node_modules/zod-validation-error": {
+ "version": "4.0.2",
+ "resolved": "https://registry.npmjs.org/zod-validation-error/-/zod-validation-error-4.0.2.tgz",
+ "integrity": "sha512-Q6/nZLe6jxuU80qb/4uJ4t5v2VEZ44lzQjPDhYJNztRQ4wyWc6VF3D3Kb/fAuPetZQnhS3hnajCf9CsWesghLQ==",
+ "dev": true,
+ "engines": {
+ "node": ">=18.0.0"
+ },
+ "peerDependencies": {
+ "zod": "^3.25.0 || ^4.0.0"
+ }
+ }
+ }
+}
diff --git a/package.json b/package.json
new file mode 100644
index 0000000..aedf1ce
--- /dev/null
+++ b/package.json
@@ -0,0 +1,32 @@
+{
+ "name": "web",
+ "private": true,
+ "version": "0.0.0",
+ "type": "module",
+ "scripts": {
+ "dev": "vite --host 0.0.0.0",
+ "build": "tsc -b && vite build",
+ "lint": "eslint .",
+ "preview": "vite preview"
+ },
+ "dependencies": {
+ "react": "^19.2.0",
+ "react-dom": "^19.2.0",
+ "pyodide": "^0.27.4"
+ },
+ "devDependencies": {
+ "@eslint/js": "^9.39.1",
+ "@types/node": "^24.10.1",
+ "@types/react": "^19.2.5",
+ "@types/react-dom": "^19.2.3",
+ "@vitejs/plugin-react": "^5.1.1",
+ "eslint": "^9.39.1",
+ "eslint-plugin-react-hooks": "^7.0.1",
+ "eslint-plugin-react-refresh": "^0.4.24",
+ "globals": "^16.5.0",
+ "typescript": "~5.9.3",
+ "typescript-eslint": "^8.46.4",
+ "vite": "^7.2.4",
+ "vite-plugin-static-copy": "^3.1.4"
+ }
+}
diff --git a/public/pystitch-1.0.0-py3-none-any.whl b/public/pystitch-1.0.0-py3-none-any.whl
new file mode 100644
index 0000000..a91b9af
Binary files /dev/null and b/public/pystitch-1.0.0-py3-none-any.whl differ
diff --git a/public/vite.svg b/public/vite.svg
new file mode 100644
index 0000000..e7b8dfb
--- /dev/null
+++ b/public/vite.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="31.88" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257"><defs><linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"></stop><stop offset="100%" stop-color="#BD34FE"></stop></linearGradient><linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"></stop><stop offset="8.333%" stop-color="#FFDD35"></stop><stop offset="100%" stop-color="#FFA800"></stop></linearGradient></defs><path fill="url(#IconifyId1813088fe1fbc01fb466)" d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"></path><path fill="url(#IconifyId1813088fe1fbc01fb467)" d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"></path></svg>
\ No newline at end of file
diff --git a/src/App.css b/src/App.css
new file mode 100644
index 0000000..7ab2332
--- /dev/null
+++ b/src/App.css
@@ -0,0 +1,645 @@
+:root {
+ --primary-color: #0066cc;
+ --secondary-color: #6c757d;
+ --danger-color: #dc3545;
+ --success-color: #28a745;
+ --warning-color: #ffc107;
+ --info-color: #17a2b8;
+ --background: #f5f5f5;
+ --panel-background: #ffffff;
+ --border-color: #dee2e6;
+ --text-color: #212529;
+ --text-muted: #6c757d;
+}
+
+* {
+ box-sizing: border-box;
+ margin: 0;
+ padding: 0;
+}
+
+body {
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
+ 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
+ sans-serif;
+ -webkit-font-smoothing: antialiased;
+ -moz-osx-font-smoothing: grayscale;
+ background-color: var(--background);
+ color: var(--text-color);
+}
+
+.app {
+ min-height: 100vh;
+ display: flex;
+ flex-direction: column;
+}
+
+.app-header {
+ background-color: var(--panel-background);
+ padding: 1.5rem 2rem;
+ border-bottom: 1px solid var(--border-color);
+ box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
+}
+
+.app-header h1 {
+ font-size: 1.75rem;
+ font-weight: 600;
+ margin-bottom: 0.5rem;
+}
+
+.error-message {
+ background-color: #f8d7da;
+ color: #721c24;
+ padding: 0.75rem 1rem;
+ border-radius: 4px;
+ margin-top: 1rem;
+ border: 1px solid #f5c6cb;
+}
+
+.app-content {
+ flex: 1;
+ display: grid;
+ grid-template-columns: 400px 1fr;
+ gap: 1.5rem;
+ padding: 1.5rem;
+ max-width: 1600px;
+ width: 100%;
+ margin: 0 auto;
+}
+
+.left-panel {
+ display: flex;
+ flex-direction: column;
+ gap: 1.5rem;
+}
+
+.right-panel {
+ display: flex;
+ flex-direction: column;
+}
+
+.connection-panel,
+.file-upload-panel,
+.progress-panel,
+.canvas-panel {
+ background-color: var(--panel-background);
+ padding: 1.5rem;
+ border-radius: 8px;
+ box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
+}
+
+h2 {
+ font-size: 1.25rem;
+ font-weight: 600;
+ margin-bottom: 1rem;
+ padding-bottom: 0.5rem;
+ border-bottom: 2px solid var(--border-color);
+}
+
+h3 {
+ font-size: 1rem;
+ font-weight: 600;
+ margin: 1rem 0 0.5rem 0;
+}
+
+.detail-row {
+ display: flex;
+ justify-content: space-between;
+ padding: 0.5rem 0;
+ border-bottom: 1px solid var(--border-color);
+}
+
+.detail-row:last-child {
+ border-bottom: none;
+}
+
+.detail-row .label {
+ font-weight: 500;
+ color: var(--text-muted);
+}
+
+.detail-row .value {
+ font-weight: 600;
+}
+
+.status-bar {
+ display: flex;
+ align-items: center;
+ gap: 1rem;
+ margin-bottom: 1rem;
+ padding: 0.75rem;
+ background-color: var(--background);
+ border-radius: 4px;
+}
+
+.status-indicator {
+ padding: 0.5rem 1rem;
+ border-radius: 4px;
+ font-weight: 600;
+ font-size: 0.875rem;
+ text-transform: uppercase;
+}
+
+.status-indicator.status-16 { /* IDLE */
+ background-color: #d1ecf1;
+ color: #0c5460;
+}
+
+.status-indicator.status-17 { /* SEWING_WAIT */
+ background-color: #d4edda;
+ color: #155724;
+}
+
+.status-indicator.status-48 { /* SEWING */
+ background-color: #fff3cd;
+ color: #856404;
+}
+
+.status-indicator.status-49 { /* SEWING_COMPLETE */
+ background-color: #d4edda;
+ color: #155724;
+}
+
+.status-indicator.status-64 { /* COLOR_CHANGE_WAIT */
+ background-color: #fff3cd;
+ color: #856404;
+}
+
+.error-indicator {
+ background-color: #f8d7da;
+ color: #721c24;
+ padding: 0.5rem 1rem;
+ border-radius: 4px;
+ font-weight: 600;
+ font-size: 0.875rem;
+}
+
+.polling-indicator {
+ color: var(--primary-color);
+ font-size: 0.75rem;
+ animation: pulse 1s ease-in-out infinite;
+}
+
+@keyframes pulse {
+ 0%, 100% {
+ opacity: 1;
+ }
+ 50% {
+ opacity: 0.3;
+ }
+}
+
+.connection-actions,
+.progress-actions {
+ display: flex;
+ gap: 0.75rem;
+ margin-top: 1rem;
+ flex-wrap: wrap;
+}
+
+button {
+ padding: 0.75rem 1.5rem;
+ border: none;
+ border-radius: 4px;
+ font-size: 0.875rem;
+ font-weight: 600;
+ cursor: pointer;
+ transition: all 0.2s;
+}
+
+button:disabled {
+ opacity: 0.5;
+ cursor: not-allowed;
+}
+
+.btn-primary {
+ background-color: var(--primary-color);
+ color: white;
+}
+
+.btn-primary:hover:not(:disabled) {
+ background-color: #0052a3;
+}
+
+.btn-secondary {
+ background-color: var(--secondary-color);
+ color: white;
+}
+
+.btn-secondary:hover:not(:disabled) {
+ background-color: #5a6268;
+}
+
+.btn-danger {
+ background-color: var(--danger-color);
+ color: white;
+}
+
+.btn-danger:hover:not(:disabled) {
+ background-color: #c82333;
+}
+
+.file-input {
+ display: none;
+}
+
+.progress-bar {
+ height: 8px;
+ background-color: var(--border-color);
+ border-radius: 4px;
+ overflow: hidden;
+ margin: 1rem 0;
+}
+
+.progress-fill {
+ height: 100%;
+ background-color: var(--primary-color);
+ transition: width 0.3s;
+}
+
+.pattern-canvas {
+ width: 100%;
+ height: 600px;
+ border: 1px solid var(--border-color);
+ border-radius: 4px;
+ background-color: #fafafa;
+}
+
+.canvas-placeholder {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ height: 600px;
+ color: var(--text-muted);
+ font-style: italic;
+}
+
+.status-message {
+ padding: 1rem;
+ border-radius: 4px;
+ margin: 1rem 0;
+ font-weight: 500;
+}
+
+.status-message.success {
+ background-color: #d4edda;
+ color: #155724;
+ border: 1px solid #c3e6cb;
+}
+
+.status-message.warning {
+ background-color: #fff3cd;
+ color: #856404;
+ border: 1px solid #ffeeba;
+}
+
+.status-message.info {
+ background-color: #d1ecf1;
+ color: #0c5460;
+ border: 1px solid #bee5eb;
+}
+
+/* Color Block Progress Styles */
+.color-blocks {
+ margin-top: 1.5rem;
+ padding-top: 1rem;
+ border-top: 1px solid var(--border-color);
+}
+
+.color-block-list {
+ display: flex;
+ flex-direction: column;
+ gap: 0.5rem;
+}
+
+.color-block-item {
+ padding: 0.75rem;
+ border-radius: 4px;
+ background-color: var(--background);
+ border: 2px solid transparent;
+ transition: all 0.3s;
+}
+
+.color-block-item.completed {
+ border-color: var(--success-color);
+ background-color: #f0f9f4;
+}
+
+.color-block-item.current {
+ border-color: var(--primary-color);
+ background-color: #e7f3ff;
+ box-shadow: 0 2px 8px rgba(0, 102, 204, 0.2);
+}
+
+.color-block-item.pending {
+ opacity: 0.6;
+}
+
+.block-header {
+ display: flex;
+ align-items: center;
+ gap: 0.75rem;
+}
+
+.color-swatch {
+ width: 24px;
+ height: 24px;
+ border-radius: 4px;
+ border: 2px solid var(--border-color);
+ box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
+}
+
+.block-label {
+ font-weight: 600;
+ flex: 1;
+}
+
+.block-status {
+ font-size: 1.25rem;
+ font-weight: bold;
+ color: var(--text-muted);
+}
+
+.color-block-item.completed .block-status {
+ color: var(--success-color);
+}
+
+.color-block-item.current .block-status {
+ color: var(--primary-color);
+}
+
+.block-stitches {
+ font-size: 0.875rem;
+ color: var(--text-muted);
+}
+
+.block-progress-bar {
+ margin-top: 0.5rem;
+ height: 4px;
+ background-color: #fff;
+ border-radius: 2px;
+ overflow: hidden;
+}
+
+.block-progress-fill {
+ height: 100%;
+ background-color: var(--primary-color);
+ transition: width 0.3s;
+}
+
+@media (max-width: 1024px) {
+ .app-content {
+ grid-template-columns: 1fr;
+ }
+}
+
+/* ==========================================================================
+ State-Based UX Safety Styles
+ ========================================================================== */
+
+/* Confirmation Dialog Styles */
+.confirm-dialog-overlay {
+ position: fixed;
+ top: 0;
+ left: 0;
+ right: 0;
+ bottom: 0;
+ background-color: rgba(0, 0, 0, 0.5);
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ z-index: 1000;
+}
+
+.confirm-dialog {
+ background-color: var(--panel-background);
+ border-radius: 8px;
+ box-shadow: 0 4px 16px rgba(0, 0, 0, 0.3);
+ max-width: 500px;
+ width: 90%;
+ margin: 1rem;
+}
+
+.confirm-dialog-danger {
+ border-top: 4px solid #dc3545;
+}
+
+.confirm-dialog-warning {
+ border-top: 4px solid #ffc107;
+}
+
+.confirm-dialog-header {
+ padding: 1.5rem;
+ border-bottom: 1px solid var(--border-color);
+}
+
+.confirm-dialog-header h3 {
+ margin: 0;
+ font-size: 1.25rem;
+ font-weight: 600;
+}
+
+.confirm-dialog-body {
+ padding: 1.5rem;
+}
+
+.confirm-dialog-body p {
+ margin: 0;
+ line-height: 1.5;
+ color: var(--text-color);
+}
+
+.confirm-dialog-actions {
+ padding: 1rem 1.5rem;
+ display: flex;
+ gap: 0.75rem;
+ justify-content: flex-end;
+ border-top: 1px solid var(--border-color);
+}
+
+/* Enhanced Status Badge */
+.status-badge {
+ display: flex;
+ align-items: center;
+ gap: 0.5rem;
+ padding: 0.5rem 1rem;
+ border-radius: 4px;
+ font-weight: 600;
+ font-size: 0.875rem;
+}
+
+.status-badge-idle,
+.status-badge-info {
+ background-color: #d1ecf1;
+ color: #0c5460;
+ border: 1px solid #bee5eb;
+}
+
+.status-badge-active,
+.status-badge-waiting,
+.status-badge-warning {
+ background-color: #fff3cd;
+ color: #856404;
+ border: 1px solid #ffeeba;
+}
+
+.status-badge-complete,
+.status-badge-success {
+ background-color: #d4edda;
+ color: #155724;
+ border: 1px solid #c3e6cb;
+}
+
+.status-badge-interrupted,
+.status-badge-error,
+.status-badge-danger {
+ background-color: #f8d7da;
+ color: #721c24;
+ border: 1px solid #f5c6cb;
+}
+
+.status-icon {
+ font-size: 1.1rem;
+ line-height: 1;
+}
+
+.status-text {
+ text-transform: uppercase;
+ letter-spacing: 0.5px;
+}
+
+/* State Indicator Component */
+.state-indicator {
+ display: flex;
+ align-items: center;
+ gap: 1rem;
+ padding: 1rem;
+ border-radius: 8px;
+ margin: 1rem 0;
+ border-left: 4px solid transparent;
+}
+
+.state-indicator-idle,
+.state-indicator-info {
+ background-color: #e7f3ff;
+ border-left-color: #0066cc;
+}
+
+.state-indicator-active,
+.state-indicator-waiting,
+.state-indicator-warning {
+ background-color: #fff8e1;
+ border-left-color: #ffc107;
+}
+
+.state-indicator-complete,
+.state-indicator-success {
+ background-color: #e8f5e9;
+ border-left-color: #28a745;
+}
+
+.state-indicator-interrupted,
+.state-indicator-error,
+.state-indicator-danger {
+ background-color: #ffebee;
+ border-left-color: #dc3545;
+}
+
+.state-icon {
+ font-size: 2rem;
+ line-height: 1;
+}
+
+.state-info {
+ flex: 1;
+}
+
+.state-label {
+ font-weight: 600;
+ font-size: 1rem;
+ margin-bottom: 0.25rem;
+}
+
+.state-description {
+ font-size: 0.875rem;
+ color: #666;
+}
+
+/* Enhanced Progress Visualization */
+.progress-bar {
+ height: 12px;
+ background-color: var(--border-color);
+ border-radius: 6px;
+ overflow: hidden;
+ margin: 1rem 0;
+ position: relative;
+ box-shadow: inset 0 1px 3px rgba(0, 0, 0, 0.1);
+}
+
+.progress-fill {
+ height: 100%;
+ background: linear-gradient(90deg, var(--primary-color), #0052a3);
+ transition: width 0.3s ease;
+ position: relative;
+ overflow: hidden;
+}
+
+.progress-fill::after {
+ content: '';
+ position: absolute;
+ top: 0;
+ left: 0;
+ right: 0;
+ bottom: 0;
+ background: linear-gradient(
+ 90deg,
+ transparent,
+ rgba(255, 255, 255, 0.3),
+ transparent
+ );
+ animation: shimmer 2s infinite;
+}
+
+@keyframes shimmer {
+ 0% {
+ transform: translateX(-100%);
+ }
+ 100% {
+ transform: translateX(100%);
+ }
+}
+
+/* Button enhancements for safety */
+button.btn-danger:hover:not(:disabled) {
+ background-color: #c82333;
+ box-shadow: 0 2px 8px rgba(220, 53, 69, 0.3);
+ transform: translateY(-1px);
+}
+
+button.btn-primary:hover:not(:disabled) {
+ box-shadow: 0 2px 8px rgba(0, 102, 204, 0.3);
+ transform: translateY(-1px);
+}
+
+button.btn-secondary:hover:not(:disabled) {
+ box-shadow: 0 2px 8px rgba(108, 117, 125, 0.3);
+ transform: translateY(-1px);
+}
+
+button:active:not(:disabled) {
+ transform: translateY(0);
+}
+
+/* Info message styles */
+.status-message.info {
+ background-color: #d1ecf1;
+ color: #0c5460;
+ border: 1px solid #bee5eb;
+}
+
+/* Enhanced visual feedback for disabled state */
+button:disabled {
+ opacity: 0.5;
+ cursor: not-allowed;
+ filter: grayscale(0.3);
+}
diff --git a/src/App.tsx b/src/App.tsx
new file mode 100644
index 0000000..248c4d4
--- /dev/null
+++ b/src/App.tsx
@@ -0,0 +1,107 @@
+import { useState, useEffect } from 'react';
+import { useBrotherMachine } from './hooks/useBrotherMachine';
+import { MachineConnection } from './components/MachineConnection';
+import { FileUpload } from './components/FileUpload';
+import { PatternCanvas } from './components/PatternCanvas';
+import { ProgressMonitor } from './components/ProgressMonitor';
+import type { PesPatternData } from './utils/pystitchConverter';
+import { pyodideLoader } from './utils/pyodideLoader';
+import './App.css';
+
+function App() {
+ const machine = useBrotherMachine();
+ const [pesData, setPesData] = useState<PesPatternData | null>(null);
+ const [pyodideReady, setPyodideReady] = useState(false);
+ const [pyodideError, setPyodideError] = useState<string | null>(null);
+
+ // Initialize Pyodide on mount
+ useEffect(() => {
+ pyodideLoader
+ .initialize()
+ .then(() => {
+ setPyodideReady(true);
+ console.log('[App] Pyodide initialized successfully');
+ })
+ .catch((err) => {
+ setPyodideError(err instanceof Error ? err.message : 'Failed to initialize Python environment');
+ console.error('[App] Failed to initialize Pyodide:', err);
+ });
+ }, []);
+
+ // Auto-load cached pattern when available
+ useEffect(() => {
+ if (machine.resumedPattern && !pesData) {
+ console.log('[App] Loading resumed pattern:', machine.resumeFileName);
+ setPesData(machine.resumedPattern);
+ }
+ }, [machine.resumedPattern, pesData, machine.resumeFileName]);
+
+ const handlePatternLoaded = (data: PesPatternData) => {
+ setPesData(data);
+ };
+
+ return (
+ <div className="app">
+ <header className="app-header">
+ <h1>Brother Embroidery Machine Controller</h1>
+ {machine.error && (
+ <div className="error-message">{machine.error}</div>
+ )}
+ {pyodideError && (
+ <div className="error-message">Python Error: {pyodideError}</div>
+ )}
+ {!pyodideReady && !pyodideError && (
+ <div className="info-message">Initializing Python environment...</div>
+ )}
+ </header>
+
+ <div className="app-content">
+ <div className="left-panel">
+ <MachineConnection
+ isConnected={machine.isConnected}
+ machineInfo={machine.machineInfo}
+ machineStatus={machine.machineStatus}
+ machineStatusName={machine.machineStatusName}
+ machineError={machine.machineError}
+ isPolling={machine.isPolling}
+ resumeAvailable={machine.resumeAvailable}
+ resumeFileName={machine.resumeFileName}
+ onConnect={machine.connect}
+ onDisconnect={machine.disconnect}
+ onRefresh={machine.refreshStatus}
+ />
+
+ <FileUpload
+ isConnected={machine.isConnected}
+ machineStatus={machine.machineStatus}
+ uploadProgress={machine.uploadProgress}
+ onPatternLoaded={handlePatternLoaded}
+ onUpload={machine.uploadPattern}
+ pyodideReady={pyodideReady}
+ />
+
+ <ProgressMonitor
+ machineStatus={machine.machineStatus}
+ patternInfo={machine.patternInfo}
+ sewingProgress={machine.sewingProgress}
+ pesData={pesData}
+ onStartMaskTrace={machine.startMaskTrace}
+ onStartSewing={machine.startSewing}
+ onResumeSewing={machine.resumeSewing}
+ onDeletePattern={machine.deletePattern}
+ />
+ </div>
+
+ <div className="right-panel">
+ <PatternCanvas
+ pesData={pesData}
+ sewingProgress={machine.sewingProgress}
+ machineInfo={machine.machineInfo}
+ />
+ </div>
+ </div>
+ </div>
+ );
+}
+
+export default App;
diff --git a/src/assets/react.svg b/src/assets/react.svg
new file mode 100644
index 0000000..6c87de9
--- /dev/null
+++ b/src/assets/react.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="35.93" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 228"><path fill="#00D8FF" d="M210.483 73.824a171.49 171.49 0 0 0-8.24-2.597c.465-1.9.893-3.777 1.273-5.621c6.238-30.281 2.16-54.676-11.769-62.708c-13.355-7.7-35.196.329-57.254 19.526a171.23 171.23 0 0 0-6.375 5.848a155.866 155.866 0 0 0-4.241-3.917C100.759 3.829 77.587-4.822 63.673 3.233C50.33 10.957 46.379 33.89 51.995 62.588a170.974 170.974 0 0 0 1.892 8.48c-3.28.932-6.445 1.924-9.474 2.98C17.309 83.498 0 98.307 0 113.668c0 15.865 18.582 31.778 46.812 41.427a145.52 145.52 0 0 0 6.921 2.165a167.467 167.467 0 0 0-2.01 9.138c-5.354 28.2-1.173 50.591 12.134 58.266c13.744 7.926 36.812-.22 59.273-19.855a145.567 145.567 0 0 0 5.342-4.923a168.064 168.064 0 0 0 6.92 6.314c21.758 18.722 43.246 26.282 56.54 18.586c13.731-7.949 18.194-32.003 12.4-61.268a145.016 145.016 0 0 0-1.535-6.842c1.62-.48 3.21-.974 4.76-1.488c29.348-9.723 48.443-25.443 48.443-41.52c0-15.417-17.868-30.326-45.517-39.844Zm-6.365 70.984c-1.4.463-2.836.91-4.3 1.345c-3.24-10.257-7.612-21.163-12.963-32.432c5.106-11 9.31-21.767 12.459-31.957c2.619.758 5.16 1.557 7.61 2.4c23.69 8.156 38.14 20.213 38.14 29.504c0 9.896-15.606 22.743-40.946 31.14Zm-10.514 20.834c2.562 12.94 2.927 24.64 1.23 33.787c-1.524 8.219-4.59 13.698-8.382 15.893c-8.067 4.67-25.32-1.4-43.927-17.412a156.726 156.726 0 0 1-6.437-5.87c7.214-7.889 14.423-17.06 21.459-27.246c12.376-1.098 24.068-2.894 34.671-5.345a134.17 134.17 0 0 1 1.386 6.193ZM87.276 214.515c-7.882 2.783-14.16 2.863-17.955.675c-8.075-4.657-11.432-22.636-6.853-46.752a156.923 156.923 0 0 1 1.869-8.499c10.486 2.32 22.093 3.988 34.498 4.994c7.084 9.967 14.501 19.128 21.976 27.15a134.668 134.668 0 0 1-4.877 4.492c-9.933 8.682-19.886 14.842-28.658 17.94ZM50.35 144.747c-12.483-4.267-22.792-9.812-29.858-15.863c-6.35-5.437-9.555-10.836-9.555-15.216c0-9.322 13.897-21.212 37.076-29.293c2.813-.98 5.757-1.905 8.812-2.773c3.204 10.42 7.406 21.315 12.477 32.332c-5.137 11.18-9.399 22.249-12.634 32.792a134.718 134.718 0 0 1-6.318-1.979Zm12.378-84.26c-4.811-24.587-1.616-43.134 6.425-47.789c8.564-4.958 27.502 2.111 47.463 19.835a144.318 144.318 0 0 1 3.841 3.545c-7.438 7.987-14.787 17.08-21.808 26.988c-12.04 1.116-23.565 2.908-34.161 5.309a160.342 160.342 0 0 1-1.76-7.887Zm110.427 27.268a347.8 347.8 0 0 0-7.785-12.803c8.168 1.033 15.994 2.404 23.343 4.08c-2.206 7.072-4.956 14.465-8.193 22.045a381.151 381.151 0 0 0-7.365-13.322Zm-45.032-43.861c5.044 5.465 10.096 11.566 15.065 18.186a322.04 322.04 0 0 0-30.257-.006c4.974-6.559 10.069-12.652 15.192-18.18ZM82.802 87.83a323.167 323.167 0 0 0-7.227 13.238c-3.184-7.553-5.909-14.98-8.134-22.152c7.304-1.634 15.093-2.97 23.209-3.984a321.524 321.524 0 0 0-7.848 12.897Zm8.081 65.352c-8.385-.936-16.291-2.203-23.593-3.793c2.26-7.3 5.045-14.885 8.298-22.6a321.187 321.187 0 0 0 7.257 13.246c2.594 4.48 5.28 8.868 8.038 13.147Zm37.542 31.03c-5.184-5.592-10.354-11.779-15.403-18.433c4.902.192 9.899.29 14.978.29c5.218 0 10.376-.117 15.453-.343c-4.985 6.774-10.018 12.97-15.028 18.486Zm52.198-57.817c3.422 7.8 6.306 15.345 8.596 22.52c-7.422 1.694-15.436 3.058-23.88 4.071a382.417 382.417 0 0 0 7.859-13.026a347.403 347.403 0 0 0 7.425-13.565Zm-16.898 8.101a358.557 358.557 0 0 1-12.281 19.815a329.4 329.4 0 0 1-23.444.823c-7.967 0-15.716-.248-23.178-.732a310.202 310.202 0 0 1-12.513-19.846h.001a307.41 307.41 0 0 1-10.923-20.627a310.278 310.278 0 0 1 10.89-20.637l-.001.001a307.318 307.318 0 0 1 12.413-19.761c7.613-.576 15.42-.876 23.31-.876H128c7.926 0 15.743.303 23.354.883a329.357 329.357 0 0 1 12.335 19.695a358.489 358.489 0 0 1 11.036 20.54a329.472 329.472 0 0 1-11 20.722Zm22.56-122.124c8.572 4.944 11.906 24.881 6.52 51.026c-.344 1.668-.73 3.367-1.15 5.09c-10.622-2.452-22.155-4.275-34.23-5.408c-7.034-10.017-14.323-19.124-21.64-27.008a160.789 160.789 0 0 1 5.888-5.4c18.9-16.447 36.564-22.941 44.612-18.3ZM128 90.808c12.625 0 22.86 10.235 22.86 22.86s-10.235 22.86-22.86 22.86s-22.86-10.235-22.86-22.86s10.235-22.86 22.86-22.86Z"></path></svg>
\ No newline at end of file
diff --git a/src/components/ConfirmDialog.tsx b/src/components/ConfirmDialog.tsx
new file mode 100644
index 0000000..9fb27a2
--- /dev/null
+++ b/src/components/ConfirmDialog.tsx
@@ -0,0 +1,73 @@
+import { useEffect, useCallback } from 'react';
+
+interface ConfirmDialogProps {
+ isOpen: boolean;
+ title: string;
+ message: string;
+ confirmText?: string;
+ cancelText?: string;
+ onConfirm: () => void;
+ onCancel: () => void;
+ variant?: 'danger' | 'warning';
+}
+
+export function ConfirmDialog({
+ isOpen,
+ title,
+ message,
+ confirmText = 'Confirm',
+ cancelText = 'Cancel',
+ onConfirm,
+ onCancel,
+ variant = 'warning',
+}: ConfirmDialogProps) {
+ // Handle escape key
+ const handleEscape = useCallback((e: KeyboardEvent) => {
+ if (e.key === 'Escape') {
+ onCancel();
+ }
+ }, [onCancel]);
+
+ useEffect(() => {
+ if (isOpen) {
+ document.addEventListener('keydown', handleEscape);
+ return () => document.removeEventListener('keydown', handleEscape);
+ }
+ }, [isOpen, handleEscape]);
+
+ if (!isOpen) return null;
+
+ return (
+ <div className="confirm-dialog-overlay" onClick={onCancel}>
+ <div
+ className={`confirm-dialog confirm-dialog-${variant}`}
+ onClick={(e) => e.stopPropagation()}
+ role="dialog"
+ aria-labelledby="dialog-title"
+ aria-describedby="dialog-message"
+ >
+ <div className="confirm-dialog-header">
+ <h3 id="dialog-title">{title}</h3>
+ </div>
+ <div className="confirm-dialog-body">
+ <p id="dialog-message">{message}</p>
+ </div>
+ <div className="confirm-dialog-actions">
+ <button
+ onClick={onCancel}
+ className="btn-secondary"
+ autoFocus
+ >
+ {cancelText}
+ </button>
+ <button
+ onClick={onConfirm}
+ className={variant === 'danger' ? 'btn-danger' : 'btn-primary'}
+ >
+ {confirmText}
+ </button>
+ </div>
+ </div>
+ </div>
+ );
+}
diff --git a/src/components/FileUpload.tsx b/src/components/FileUpload.tsx
new file mode 100644
index 0000000..76a62bf
--- /dev/null
+++ b/src/components/FileUpload.tsx
@@ -0,0 +1,136 @@
+import { useState, useCallback } from 'react';
+import { convertPesToPen, type PesPatternData } from '../utils/pystitchConverter';
+import { MachineStatus } from '../types/machine';
+import { canUploadPattern, getMachineStateCategory } from '../utils/machineStateHelpers';
+
+interface FileUploadProps {
+ isConnected: boolean;
+ machineStatus: MachineStatus;
+ uploadProgress: number;
+ onPatternLoaded: (pesData: PesPatternData) => void;
+ onUpload: (penData: Uint8Array, pesData: PesPatternData, fileName: string) => void;
+ pyodideReady: boolean;
+}
+
+export function FileUpload({
+ isConnected,
+ machineStatus,
+ uploadProgress,
+ onPatternLoaded,
+ onUpload,
+ pyodideReady,
+}: FileUploadProps) {
+ const [pesData, setPesData] = useState<PesPatternData | null>(null);
+ const [fileName, setFileName] = useState<string>('');
+ const [isLoading, setIsLoading] = useState(false);
+
+ const handleFileChange = useCallback(
+ async (event: React.ChangeEvent<HTMLInputElement>) => {
+ const file = event.target.files?.[0];
+ if (!file) return;
+
+ if (!pyodideReady) {
+ alert('Python environment is still loading. Please wait...');
+ return;
+ }
+
+ setIsLoading(true);
+ try {
+ const data = await convertPesToPen(file);
+ setPesData(data);
+ setFileName(file.name);
+ onPatternLoaded(data);
+ } catch (err) {
+ alert(
+ `Failed to load PES file: ${
+ err instanceof Error ? err.message : 'Unknown error'
+ }`
+ );
+ } finally {
+ setIsLoading(false);
+ }
+ },
+ [onPatternLoaded, pyodideReady]
+ );
+
+ const handleUpload = useCallback(() => {
+ if (pesData && fileName) {
+ onUpload(pesData.penData, pesData, fileName);
+ }
+ }, [pesData, fileName, onUpload]);
+
+ return (
+ <div className="file-upload-panel">
+ <h2>Pattern File</h2>
+
+ <div className="upload-controls">
+ <input
+ type="file"
+ accept=".pes"
+ onChange={handleFileChange}
+ id="file-input"
+ className="file-input"
+ disabled={!pyodideReady || isLoading}
+ />
+ <label htmlFor="file-input" className={`btn-secondary ${!pyodideReady || isLoading ? 'disabled' : ''}`}>
+ {isLoading ? 'Loading...' : !pyodideReady ? 'Initializing...' : 'Choose PES File'}
+ </label>
+
+ {pesData && (
+ <div className="pattern-info">
+ <h3>Pattern Details</h3>
+ <div className="detail-row">
+ <span className="label">Total Stitches:</span>
+ <span className="value">{pesData.stitchCount}</span>
+ </div>
+ <div className="detail-row">
+ <span className="label">Colors:</span>
+ <span className="value">{pesData.colorCount}</span>
+ </div>
+ <div className="detail-row">
+ <span className="label">Size:</span>
+ <span className="value">
+ {((pesData.bounds.maxX - pesData.bounds.minX) / 10).toFixed(1)} x{' '}
+ {((pesData.bounds.maxY - pesData.bounds.minY) / 10).toFixed(1)} mm
+ </span>
+ </div>
+ <div className="detail-row">
+ <span className="label">Bounds:</span>
+ <span className="value">
+ ({pesData.bounds.minX}, {pesData.bounds.minY}) to (
+ {pesData.bounds.maxX}, {pesData.bounds.maxY})
+ </span>
+ </div>
+ </div>
+ )}
+
+ {pesData && canUploadPattern(machineStatus) && (
+ <button
+ onClick={handleUpload}
+ disabled={!isConnected || uploadProgress > 0}
+ className="btn-primary"
+ >
+ {uploadProgress > 0
+ ? `Uploading... ${uploadProgress.toFixed(0)}%`
+ : 'Upload to Machine'}
+ </button>
+ )}
+
+ {pesData && !canUploadPattern(machineStatus) && (
+ <div className="status-message warning">
+ Cannot upload pattern while machine is {getMachineStateCategory(machineStatus)}
+ </div>
+ )}
+
+ {uploadProgress > 0 && uploadProgress < 100 && (
+ <div className="progress-bar">
+ <div
+ className="progress-fill"
+ style={{ width: `${uploadProgress}%` }}
+ />
+ </div>
+ )}
+ </div>
+ </div>
+ );
+}
diff --git a/src/components/MachineConnection.tsx b/src/components/MachineConnection.tsx
new file mode 100644
index 0000000..d2507dd
--- /dev/null
+++ b/src/components/MachineConnection.tsx
@@ -0,0 +1,134 @@
+import { useState } from 'react';
+import type { MachineInfo } from '../types/machine';
+import { MachineStatus } from '../types/machine';
+import { ConfirmDialog } from './ConfirmDialog';
+import { shouldConfirmDisconnect, getStateVisualInfo } from '../utils/machineStateHelpers';
+import { hasError, getErrorMessage } from '../utils/errorCodeHelpers';
+
+interface MachineConnectionProps {
+ isConnected: boolean;
+ machineInfo: MachineInfo | null;
+ machineStatus: MachineStatus;
+ machineStatusName: string;
+ machineError: number;
+ isPolling: boolean;
+ resumeAvailable: boolean;
+ resumeFileName: string | null;
+ onConnect: () => void;
+ onDisconnect: () => void;
+ onRefresh: () => void;
+}
+
+export function MachineConnection({
+ isConnected,
+ machineInfo,
+ machineStatus,
+ machineStatusName,
+ machineError,
+ isPolling,
+ resumeAvailable,
+ resumeFileName,
+ onConnect,
+ onDisconnect,
+ onRefresh,
+}: MachineConnectionProps) {
+ const [showDisconnectConfirm, setShowDisconnectConfirm] = useState(false);
+
+ const handleDisconnectClick = () => {
+ if (shouldConfirmDisconnect(machineStatus)) {
+ setShowDisconnectConfirm(true);
+ } else {
+ onDisconnect();
+ }
+ };
+
+ const handleConfirmDisconnect = () => {
+ setShowDisconnectConfirm(false);
+ onDisconnect();
+ };
+
+ const stateVisual = getStateVisualInfo(machineStatus);
+
+ return (
+ <div className="connection-panel">
+ <h2>Machine Connection</h2>
+
+ {!isConnected ? (
+ <div className="connection-actions">
+ <button onClick={onConnect} className="btn-primary">
+ Connect to Machine
+ </button>
+ </div>
+ ) : (
+ <div className="connection-info">
+ <div className="status-bar">
+ <span className={`status-badge status-badge-${stateVisual.color}`}>
+ <span className="status-icon">{stateVisual.icon}</span>
+ <span className="status-text">{machineStatusName}</span>
+ </span>
+ {isPolling && (
+ <span className="polling-indicator" title="Polling machine status">●</span>
+ )}
+ {hasError(machineError) && (
+ <span className="error-indicator">{getErrorMessage(machineError)}</span>
+ )}
+ </div>
+
+ {machineInfo && (
+ <div className="machine-details">
+ <div className="detail-row">
+ <span className="label">Model:</span>
+ <span className="value">{machineInfo.modelNumber}</span>
+ </div>
+ <div className="detail-row">
+ <span className="label">Serial:</span>
+ <span className="value">{machineInfo.serialNumber}</span>
+ </div>
+ <div className="detail-row">
+ <span className="label">Software:</span>
+ <span className="value">{machineInfo.softwareVersion}</span>
+ </div>
+ <div className="detail-row">
+ <span className="label">Max Area:</span>
+ <span className="value">
+ {(machineInfo.maxWidth / 10).toFixed(1)} x{' '}
+ {(machineInfo.maxHeight / 10).toFixed(1)} mm
+ </span>
+ </div>
+ <div className="detail-row">
+ <span className="label">MAC:</span>
+ <span className="value">{machineInfo.macAddress}</span>
+ </div>
+ </div>
+ )}
+
+ {resumeAvailable && resumeFileName && (
+ <div className="status-message success">
+ Loaded cached pattern: "{resumeFileName}"
+ </div>
+ )}
+
+ <div className="connection-actions">
+ <button onClick={onRefresh} className="btn-secondary">
+ Refresh Status
+ </button>
+ <button onClick={handleDisconnectClick} className="btn-danger">
+ Disconnect
+ </button>
+ </div>
+ </div>
+ )}
+
+ <ConfirmDialog
+ isOpen={showDisconnectConfirm}
+ title="Confirm Disconnect"
+ message={`The machine is currently ${machineStatusName.toLowerCase()}. Disconnecting may interrupt the operation. Are you sure you want to disconnect?`}
+ confirmText="Disconnect Anyway"
+ cancelText="Cancel"
+ onConfirm={handleConfirmDisconnect}
+ onCancel={() => setShowDisconnectConfirm(false)}
+ variant="danger"
+ />
+ </div>
+ );
+}
diff --git a/src/components/PatternCanvas.tsx b/src/components/PatternCanvas.tsx
new file mode 100644
index 0000000..8467109
--- /dev/null
+++ b/src/components/PatternCanvas.tsx
@@ -0,0 +1,284 @@
+import { useEffect, useRef } from 'react';
+import type { PesPatternData } from '../utils/pystitchConverter';
+import { getThreadColor } from '../utils/pystitchConverter';
+import type { SewingProgress, MachineInfo } from '../types/machine';
+
+interface PatternCanvasProps {
+ pesData: PesPatternData | null;
+ sewingProgress: SewingProgress | null;
+ machineInfo: MachineInfo | null;
+}
+
+export function PatternCanvas({ pesData, sewingProgress, machineInfo }: PatternCanvasProps) {
+ const canvasRef = useRef<HTMLCanvasElement>(null);
+
+ useEffect(() => {
+ if (!canvasRef.current || !pesData) return;
+
+ const canvas = canvasRef.current;
+ const ctx = canvas.getContext('2d');
+ if (!ctx) return;
+
+ // Clear canvas
+ ctx.clearRect(0, 0, canvas.width, canvas.height);
+
+ const currentStitch = sewingProgress?.currentStitch || 0;
+
+ const { stitches, bounds } = pesData;
+ const { minX, maxX, minY, maxY } = bounds;
+ const patternWidth = maxX - minX;
+ const patternHeight = maxY - minY;
+
+ const padding = 40;
+
+ // Calculate scale based on hoop size if available, otherwise pattern size
+ let scale: number;
+ let viewWidth: number;
+ let viewHeight: number;
+
+ if (machineInfo) {
+ // Use hoop dimensions to determine scale
+ viewWidth = machineInfo.maxWidth;
+ viewHeight = machineInfo.maxHeight;
+ } else {
+ // Fallback to pattern dimensions
+ viewWidth = patternWidth;
+ viewHeight = patternHeight;
+ }
+
+ const scaleX = (canvas.width - 2 * padding) / viewWidth;
+ const scaleY = (canvas.height - 2 * padding) / viewHeight;
+ scale = Math.min(scaleX, scaleY);
+
+ // Center the view (hoop or pattern) in canvas
+ // The origin (0,0) should be at the center of the hoop
+ const offsetX = canvas.width / 2;
+ const offsetY = canvas.height / 2;
+
+ // Draw grid
+ ctx.strokeStyle = '#e0e0e0';
+ ctx.lineWidth = 1;
+ const gridSize = 100; // 10mm grid (100 units in 0.1mm)
+
+ // Determine grid bounds based on hoop or pattern
+ const gridMinX = machineInfo ? -machineInfo.maxWidth / 2 : minX;
+ const gridMaxX = machineInfo ? machineInfo.maxWidth / 2 : maxX;
+ const gridMinY = machineInfo ? -machineInfo.maxHeight / 2 : minY;
+ const gridMaxY = machineInfo ? machineInfo.maxHeight / 2 : maxY;
+
+ for (let x = Math.floor(gridMinX / gridSize) * gridSize; x <= gridMaxX; x += gridSize) {
+ const canvasX = x * scale + offsetX;
+ ctx.beginPath();
+ ctx.moveTo(canvasX, padding);
+ ctx.lineTo(canvasX, canvas.height - padding);
+ ctx.stroke();
+ }
+ for (let y = Math.floor(gridMinY / gridSize) * gridSize; y <= gridMaxY; y += gridSize) {
+ const canvasY = y * scale + offsetY;
+ ctx.beginPath();
+ ctx.moveTo(padding, canvasY);
+ ctx.lineTo(canvas.width - padding, canvasY);
+ ctx.stroke();
+ }
+
+ // Draw origin
+ ctx.strokeStyle = '#888';
+ ctx.lineWidth = 2;
+ ctx.beginPath();
+ ctx.moveTo(offsetX - 10, offsetY);
+ ctx.lineTo(offsetX + 10, offsetY);
+ ctx.stroke();
+ ctx.beginPath();
+ ctx.moveTo(offsetX, offsetY - 10);
+ ctx.lineTo(offsetX, offsetY + 10);
+ ctx.stroke();
+
+ // Draw hoop boundary (if machine info available)
+ if (machineInfo) {
+ // Machine info stores dimensions in 0.1mm units
+ const hoopWidth = machineInfo.maxWidth;
+ const hoopHeight = machineInfo.maxHeight;
+
+ // Hoop is centered at origin (0, 0)
+ const hoopLeft = -hoopWidth / 2;
+ const hoopTop = -hoopHeight / 2;
+ const hoopRight = hoopWidth / 2;
+ const hoopBottom = hoopHeight / 2;
+
+ // Draw hoop boundary
+ ctx.strokeStyle = '#2196F3';
+ ctx.lineWidth = 3;
+ ctx.setLineDash([10, 5]);
+ ctx.strokeRect(
+ hoopLeft * scale + offsetX,
+ hoopTop * scale + offsetY,
+ hoopWidth * scale,
+ hoopHeight * scale
+ );
+
+ // Draw hoop label
+ ctx.setLineDash([]);
+ ctx.fillStyle = '#2196F3';
+ ctx.font = 'bold 14px sans-serif';
+ ctx.fillText(
+ `Hoop: ${(hoopWidth / 10).toFixed(0)} x ${(hoopHeight / 10).toFixed(0)} mm`,
+ hoopLeft * scale + offsetX + 10,
+ hoopTop * scale + offsetY + 25
+ );
+ }
+
+ // Draw stitches
+ // stitches is number[][], each stitch is [x, y, command, colorIndex]
+ const MOVE = 0x10;
+
+ ctx.lineWidth = 1.5;
+ let lastX = 0;
+ let lastY = 0;
+ let threadColor = getThreadColor(pesData, 0);
+ let currentPosX = 0;
+ let currentPosY = 0;
+
+ for (let i = 0; i < stitches.length; i++) {
+ const stitch = stitches[i];
+ const x = stitch[0] * scale + offsetX;
+ const y = stitch[1] * scale + offsetY;
+ const cmd = stitch[2];
+ const colorIndex = stitch[3]; // Color index from PyStitch
+
+ // Update thread color based on stitch's color index
+ threadColor = getThreadColor(pesData, colorIndex);
+
+ // Track current position for highlighting
+ if (i === currentStitch) {
+ currentPosX = x;
+ currentPosY = y;
+ }
+
+ if (i > 0) {
+ const isCompleted = i < currentStitch;
+ const isCurrent = i === currentStitch;
+
+ if ((cmd & MOVE) !== 0) {
+ // Draw jump as dashed line
+ ctx.strokeStyle = isCompleted ? '#cccccc' : '#e8e8e8';
+ ctx.setLineDash([3, 3]);
+ } else {
+ // Draw stitch as solid line with actual thread color
+ // Dim pending stitches
+ if (isCompleted) {
+ ctx.strokeStyle = threadColor;
+ ctx.globalAlpha = 1.0;
+ } else {
+ ctx.strokeStyle = threadColor;
+ ctx.globalAlpha = 0.3;
+ }
+ ctx.setLineDash([]);
+ }
+
+ ctx.beginPath();
+ ctx.moveTo(lastX, lastY);
+ ctx.lineTo(x, y);
+ ctx.stroke();
+ ctx.globalAlpha = 1.0;
+ }
+
+ lastX = x;
+ lastY = y;
+ }
+
+ // Draw current position indicator
+ if (currentStitch > 0 && currentStitch < stitches.length) {
+ // Draw a pulsing circle at current position
+ ctx.strokeStyle = '#ff0000';
+ ctx.fillStyle = 'rgba(255, 0, 0, 0.3)';
+ ctx.lineWidth = 3;
+ ctx.setLineDash([]);
+
+ ctx.beginPath();
+ ctx.arc(currentPosX, currentPosY, 8, 0, 2 * Math.PI);
+ ctx.fill();
+ ctx.stroke();
+
+ // Draw crosshair
+ ctx.strokeStyle = '#ff0000';
+ ctx.lineWidth = 2;
+ ctx.beginPath();
+ ctx.moveTo(currentPosX - 12, currentPosY);
+ ctx.lineTo(currentPosX - 3, currentPosY);
+ ctx.stroke();
+ ctx.beginPath();
+ ctx.moveTo(currentPosX + 12, currentPosY);
+ ctx.lineTo(currentPosX + 3, currentPosY);
+ ctx.stroke();
+ ctx.beginPath();
+ ctx.moveTo(currentPosX, currentPosY - 12);
+ ctx.lineTo(currentPosX, currentPosY - 3);
+ ctx.stroke();
+ ctx.beginPath();
+ ctx.moveTo(currentPosX, currentPosY + 12);
+ ctx.lineTo(currentPosX, currentPosY + 3);
+ ctx.stroke();
+ }
+
+ // Draw bounds
+ ctx.strokeStyle = '#ff0000';
+ ctx.lineWidth = 2;
+ ctx.setLineDash([5, 5]);
+ ctx.strokeRect(
+ minX * scale + offsetX,
+ minY * scale + offsetY,
+ patternWidth * scale,
+ patternHeight * scale
+ );
+
+ // Draw color legend using actual thread colors
+ ctx.setLineDash([]);
+ let legendY = 20;
+
+ // Draw legend for each thread
+ for (let i = 0; i < pesData.threads.length; i++) {
+ const color = getThreadColor(pesData, i);
+
+ ctx.fillStyle = color;
+ ctx.fillRect(10, legendY, 20, 20);
+ ctx.strokeStyle = '#000';
+ ctx.lineWidth = 1;
+ ctx.strokeRect(10, legendY, 20, 20);
+
+ ctx.fillStyle = '#000';
+ ctx.font = '12px sans-serif';
+ ctx.fillText(
+ `Thread ${i + 1}`,
+ 35,
+ legendY + 15
+ );
+ legendY += 25;
+ }
+
+ // Draw dimensions
+ ctx.fillStyle = '#000';
+ ctx.font = '14px sans-serif';
+ ctx.fillText(
+ `${(patternWidth / 10).toFixed(1)} x ${(patternHeight / 10).toFixed(1)} mm`,
+ canvas.width - 120,
+ canvas.height - 10
+ );
+ }, [pesData, sewingProgress, machineInfo]);
+
+ return (
+ <div className="canvas-panel">
+ <h2>Pattern Preview</h2>
+ <canvas
+ ref={canvasRef}
+ width={800}
+ height={600}
+ className="pattern-canvas"
+ />
+ {!pesData && (
+ <div className="canvas-placeholder">
+ Load a PES file to preview the pattern
+ </div>
+ )}
+ </div>
+ );
+}
diff --git a/src/components/ProgressMonitor.tsx b/src/components/ProgressMonitor.tsx
new file mode 100644
index 0000000..67b944a
--- /dev/null
+++ b/src/components/ProgressMonitor.tsx
@@ -0,0 +1,310 @@
+import type { PatternInfo, SewingProgress } from '../types/machine';
+import { MachineStatus } from '../types/machine';
+import type { PesPatternData } from '../utils/pystitchConverter';
+import {
+ canStartSewing,
+ canStartMaskTrace,
+ canDeletePattern,
+ canResumeSewing,
+ getStateVisualInfo
+} from '../utils/machineStateHelpers';
+
+interface ProgressMonitorProps {
+ machineStatus: MachineStatus;
+ patternInfo: PatternInfo | null;
+ sewingProgress: SewingProgress | null;
+ pesData: PesPatternData | null;
+ onStartMaskTrace: () => void;
+ onStartSewing: () => void;
+ onResumeSewing: () => void;
+ onDeletePattern: () => void;
+}
+
+export function ProgressMonitor({
+ machineStatus,
+ patternInfo,
+ sewingProgress,
+ pesData,
+ onStartMaskTrace,
+ onStartSewing,
+ onResumeSewing,
+ onDeletePattern,
+}: ProgressMonitorProps) {
+ // State indicators
+ const isSewing = machineStatus === MachineStatus.SEWING;
+ const isComplete = machineStatus === MachineStatus.SEWING_COMPLETE;
+ const isColorChange = machineStatus === MachineStatus.COLOR_CHANGE_WAIT;
+ const isMaskTracing = machineStatus === MachineStatus.MASK_TRACING;
+ const isMaskTraceComplete = machineStatus === MachineStatus.MASK_TRACE_COMPLETE;
+ const isMaskTraceWait = machineStatus === MachineStatus.MASK_TRACE_LOCK_WAIT;
+
+ const stateVisual = getStateVisualInfo(machineStatus);
+
+ const progressPercent = patternInfo
+ ? ((sewingProgress?.currentStitch || 0) / patternInfo.totalStitches) * 100
+ : 0;
+
+ // Calculate color block information from pesData
+ const colorBlocks = pesData ? (() => {
+ const blocks: Array<{
+ colorIndex: number;
+ threadHex: string;
+ startStitch: number;
+ endStitch: number;
+ stitchCount: number;
+ }> = [];
+
+ let currentColorIndex = pesData.stitches[0]?.[3] ?? 0;
+ let blockStartStitch = 0;
+
+ for (let i = 0; i < pesData.stitches.length; i++) {
+ const stitchColorIndex = pesData.stitches[i][3];
+
+ // When color changes, save the previous block
+ if (stitchColorIndex !== currentColorIndex || i === pesData.stitches.length - 1) {
+ const endStitch = i === pesData.stitches.length - 1 ? i + 1 : i;
+ blocks.push({
+ colorIndex: currentColorIndex,
+ threadHex: pesData.threads[currentColorIndex]?.hex || '#000000',
+ startStitch: blockStartStitch,
+ endStitch: endStitch,
+ stitchCount: endStitch - blockStartStitch,
+ });
+
+ currentColorIndex = stitchColorIndex;
+ blockStartStitch = i;
+ }
+ }
+
+ return blocks;
+ })() : [];
+
+ // Determine current color block based on current stitch
+ const currentStitch = sewingProgress?.currentStitch || 0;
+ const currentBlockIndex = colorBlocks.findIndex(
+ block => currentStitch >= block.startStitch && currentStitch < block.endStitch
+ );
+
+ return (
+ <div className="progress-panel">
+ <h2>Sewing Progress</h2>
+
+ {patternInfo && (
+ <div className="pattern-stats">
+ <div className="detail-row">
+ <span className="label">Total Stitches:</span>
+ <span className="value">{patternInfo.totalStitches}</span>
+ </div>
+ <div className="detail-row">
+ <span className="label">Estimated Time:</span>
+ <span className="value">
+ {Math.floor(patternInfo.totalTime / 60)}:
+ {(patternInfo.totalTime % 60).toString().padStart(2, '0')}
+ </span>
+ </div>
+ <div className="detail-row">
+ <span className="label">Speed:</span>
+ <span className="value">{patternInfo.speed} spm</span>
+ </div>
+ <div className="detail-row">
+ <span className="label">Bounds:</span>
+ <span className="value">
+ ({patternInfo.boundLeft}, {patternInfo.boundTop}) to (
+ {patternInfo.boundRight}, {patternInfo.boundBottom})
+ </span>
+ </div>
+ </div>
+ )}
+
+ {colorBlocks.length > 0 && (
+ <div className="color-blocks">
+ <h3>Color Blocks</h3>
+ <div className="color-block-list">
+ {colorBlocks.map((block, index) => {
+ const isCompleted = currentStitch >= block.endStitch;
+ const isCurrent = index === currentBlockIndex;
+ const isPending = currentStitch < block.startStitch;
+
+ // Calculate progress within current block
+ let blockProgress = 0;
+ if (isCurrent) {
+ blockProgress = ((currentStitch - block.startStitch) / block.stitchCount) * 100;
+ } else if (isCompleted) {
+ blockProgress = 100;
+ }
+
+ return (
+ <div
+ key={index}
+ className={`color-block-item ${
+ isCompleted ? 'completed' : isCurrent ? 'current' : 'pending'
+ }`}
+ >
+ <div className="block-header">
+ <div
+ className="color-swatch"
+ style={{ backgroundColor: block.threadHex }}
+ title={block.threadHex}
+ />
+ <span className="block-label">
+ Thread {block.colorIndex + 1}
+ </span>
+ <span className="block-status">
+ {isCompleted ? '✓' : isCurrent ? '→' : '○'}
+ </span>
+ <span className="block-stitches">
+ {block.stitchCount} stitches
+ </span>
+ </div>
+ {isCurrent && (
+ <div className="block-progress-bar">
+ <div
+ className="block-progress-fill"
+ style={{ width: `${blockProgress}%` }}
+ />
+ </div>
+ )}
+ </div>
+ );
+ })}
+ </div>
+ </div>
+ )}
+
+ {sewingProgress && (
+ <div className="sewing-stats">
+ <div className="progress-bar">
+ <div className="progress-fill" style={{ width: `${progressPercent}%` }} />
+ </div>
+
+ <div className="detail-row">
+ <span className="label">Current Stitch:</span>
+ <span className="value">
+ {sewingProgress.currentStitch} / {patternInfo?.totalStitches || 0}
+ </span>
+ </div>
+ <div className="detail-row">
+ <span className="label">Elapsed Time:</span>
+ <span className="value">
+ {Math.floor(sewingProgress.currentTime / 60)}:
+ {(sewingProgress.currentTime % 60).toString().padStart(2, '0')}
+ </span>
+ </div>
+ <div className="detail-row">
+ <span className="label">Position:</span>
+ <span className="value">
+ ({(sewingProgress.positionX / 10).toFixed(1)}mm,{' '}
+ {(sewingProgress.positionY / 10).toFixed(1)}mm)
+ </span>
+ </div>
+ <div className="detail-row">
+ <span className="label">Progress:</span>
+ <span className="value">{progressPercent.toFixed(1)}%</span>
+ </div>
+ </div>
+ )}
+
+ {/* State Visual Indicator */}
+ {patternInfo && (
+ <div className={`state-indicator state-indicator-${stateVisual.color}`}>
+ <span className="state-icon">{stateVisual.icon}</span>
+ <div className="state-info">
+ <div className="state-label">{stateVisual.label}</div>
+ <div className="state-description">{stateVisual.description}</div>
+ </div>
+ </div>
+ )}
+
+ <div className="progress-actions">
+ {/* Mask trace waiting for confirmation */}
+ {isMaskTraceWait && (
+ <div className="status-message warning">
+ Press button on machine to start mask trace
+ </div>
+ )}
+
+ {/* Mask trace in progress */}
+ {isMaskTracing && (
+ <div className="status-message info">
+ Mask trace in progress...
+ </div>
+ )}
+
+ {/* Mask trace complete - waiting for confirmation */}
+ {isMaskTraceComplete && (
+ <>
+ <div className="status-message success">
+ Mask trace complete!
+ </div>
+ <div className="status-message warning">
+ Press button on machine to confirm (or trace again)
+ </div>
+ {canStartMaskTrace(machineStatus) && (
+ <button onClick={onStartMaskTrace} className="btn-secondary">
+ Trace Again
+ </button>
+ )}
+ </>
+ )}
+
+ {/* Ready to start (pattern uploaded) */}
+ {machineStatus === MachineStatus.SEWING_WAIT && (
+ <>
+ {canStartMaskTrace(machineStatus) && (
+ <button onClick={onStartMaskTrace} className="btn-secondary">
+ Start Mask Trace
+ </button>
+ )}
+ {canStartSewing(machineStatus) && (
+ <button onClick={onStartSewing} className="btn-primary">
+ Start Sewing
+ </button>
+ )}
+ </>
+ )}
+
+ {/* Resume sewing for interrupted states */}
+ {canResumeSewing(machineStatus) && (
+ <button onClick={onResumeSewing} className="btn-primary">
+ ▶️ Resume Sewing
+ </button>
+ )}
+
+ {/* Color change needed */}
+ {isColorChange && (
+ <div className="status-message warning">
+ Waiting for color change - change thread and press button on machine
+ </div>
+ )}
+
+ {/* Sewing in progress */}
+ {isSewing && (
+ <div className="status-message info">
+ Sewing in progress...
+ </div>
+ )}
+
+ {/* Sewing complete */}
+ {isComplete && (
+ <div className="status-message success">
+ Sewing complete!
+ </div>
+ )}
+
+ {/* Delete pattern button - ONLY show when safe */}
+ {patternInfo && canDeletePattern(machineStatus) && (
+ <button onClick={onDeletePattern} className="btn-danger">
+ Delete Pattern
+ </button>
+ )}
+
+ {/* Show warning when delete is unavailable */}
+ {patternInfo && !canDeletePattern(machineStatus) && (
+ <div className="status-message info">
+ Pattern cannot be deleted during active operations
+ </div>
+ )}
+ </div>
+ </div>
+ );
+}
diff --git a/src/hooks/useBrotherMachine.ts b/src/hooks/useBrotherMachine.ts
new file mode 100644
index 0000000..350a856
--- /dev/null
+++ b/src/hooks/useBrotherMachine.ts
@@ -0,0 +1,355 @@
+import { useState, useCallback, useEffect } from "react";
+import { BrotherPP1Service } from "../services/BrotherPP1Service";
+import type {
+ MachineInfo,
+ PatternInfo,
+ SewingProgress,
+} from "../types/machine";
+import { MachineStatus, MachineStatusNames } from "../types/machine";
+import {
+ PatternCacheService,
+ uuidToString,
+} from "../services/PatternCacheService";
+import type { PesPatternData } from "../utils/pystitchConverter";
+
+export function useBrotherMachine() {
+ const [service] = useState(() => new BrotherPP1Service());
+ const [isConnected, setIsConnected] = useState(false);
+ const [machineInfo, setMachineInfo] = useState<MachineInfo | null>(null);
+ const [machineStatus, setMachineStatus] = useState<MachineStatus>(
+ MachineStatus.None,
+ );
+ const [machineError, setMachineError] = useState<number>(0);
+ const [patternInfo, setPatternInfo] = useState<PatternInfo | null>(null);
+ const [sewingProgress, setSewingProgress] = useState<SewingProgress | null>(
+ null,
+ );
+ const [uploadProgress, setUploadProgress] = useState<number>(0);
+ const [error, setError] = useState<string | null>(null);
+ const [isPolling, setIsPolling] = useState(false);
+ const [resumeAvailable, setResumeAvailable] = useState(false);
+ const [resumeFileName, setResumeFileName] = useState<string | null>(null);
+ const [resumedPattern, setResumedPattern] = useState<PesPatternData | null>(
+ null,
+ );
+
+ // Define checkResume first (before connect uses it)
+ const checkResume = useCallback(async (): Promise<PesPatternData | null> => {
+ try {
+ console.log("[Resume] Checking for cached pattern...");
+
+ // Get UUID from machine
+ const machineUuid = await service.getPatternUUID();
+
+ console.log(
+ "[Resume] Machine UUID:",
+ machineUuid ? uuidToString(machineUuid) : "none",
+ );
+
+ if (!machineUuid) {
+ console.log("[Resume] No pattern loaded on machine");
+ setResumeAvailable(false);
+ setResumeFileName(null);
+ return null;
+ }
+
+ // Check if we have this pattern cached
+ const uuidStr = uuidToString(machineUuid);
+ const cached = PatternCacheService.getPatternByUUID(uuidStr);
+
+ if (cached) {
+ console.log("[Resume] Pattern found in cache:", cached.fileName);
+ console.log("[Resume] Auto-loading cached pattern...");
+ setResumeAvailable(true);
+ setResumeFileName(cached.fileName);
+ setResumedPattern(cached.pesData);
+
+ // Fetch pattern info from machine
+ try {
+ const info = await service.getPatternInfo();
+ setPatternInfo(info);
+ console.log("[Resume] Pattern info loaded from machine");
+ } catch (err) {
+ console.error("[Resume] Failed to load pattern info:", err);
+ }
+
+ // Return the cached pattern data to be loaded
+ return cached.pesData;
+ } else {
+ console.log("[Resume] Pattern on machine not found in cache");
+ setResumeAvailable(false);
+ setResumeFileName(null);
+ return null;
+ }
+ } catch (err) {
+ console.error("[Resume] Failed to check resume:", err);
+ setResumeAvailable(false);
+ setResumeFileName(null);
+ return null;
+ }
+ }, [service]);
+
+ const connect = useCallback(async () => {
+ try {
+ setError(null);
+ await service.connect();
+ setIsConnected(true);
+
+ // Fetch initial machine info and status
+ const info = await service.getMachineInfo();
+ setMachineInfo(info);
+
+ const state = await service.getMachineState();
+ setMachineStatus(state.status);
+ setMachineError(state.error);
+
+ // Check for resume possibility
+ await checkResume();
+ } catch (err) {
+ console.log(err);
+ setError(err instanceof Error ? err.message : "Failed to connect");
+ setIsConnected(false);
+ }
+ }, [service, checkResume]);
+
+ const disconnect = useCallback(async () => {
+ try {
+ await service.disconnect();
+ setIsConnected(false);
+ setMachineInfo(null);
+ setMachineStatus(MachineStatus.None);
+ setPatternInfo(null);
+ setSewingProgress(null);
+ setError(null);
+ } catch (err) {
+ setError(err instanceof Error ? err.message : "Failed to disconnect");
+ }
+ }, [service]);
+
+ const refreshStatus = useCallback(async () => {
+ if (!isConnected) return;
+
+ try {
+ setIsPolling(true);
+ const state = await service.getMachineState();
+ setMachineStatus(state.status);
+ setMachineError(state.error);
+ } catch (err) {
+ setError(err instanceof Error ? err.message : "Failed to get status");
+ } finally {
+ setIsPolling(false);
+ }
+ }, [service, isConnected]);
+
+ const refreshPatternInfo = useCallback(async () => {
+ if (!isConnected) return;
+
+ try {
+ const info = await service.getPatternInfo();
+ setPatternInfo(info);
+ } catch (err) {
+ setError(
+ err instanceof Error ? err.message : "Failed to get pattern info",
+ );
+ }
+ }, [service, isConnected]);
+
+ const refreshProgress = useCallback(async () => {
+ if (!isConnected) return;
+
+ try {
+ const progress = await service.getSewingProgress();
+ setSewingProgress(progress);
+ } catch (err) {
+ setError(err instanceof Error ? err.message : "Failed to get progress");
+ }
+ }, [service, isConnected]);
+
+ const loadCachedPattern =
+ useCallback(async (): Promise<PesPatternData | null> => {
+ if (!resumeAvailable) return null;
+
+ try {
+ const machineUuid = await service.getPatternUUID();
+ if (!machineUuid) return null;
+
+ const uuidStr = uuidToString(machineUuid);
+ const cached = PatternCacheService.getPatternByUUID(uuidStr);
+
+ if (cached) {
+ console.log("[Resume] Loading cached pattern:", cached.fileName);
+ // Refresh pattern info from machine
+ await refreshPatternInfo();
+ return cached.pesData;
+ }
+
+ return null;
+ } catch (err) {
+ setError(
+ err instanceof Error ? err.message : "Failed to load cached pattern",
+ );
+ return null;
+ }
+ }, [service, resumeAvailable, refreshPatternInfo]);
+
+ const uploadPattern = useCallback(
+ async (penData: Uint8Array, pesData: PesPatternData, fileName: string) => {
+ if (!isConnected) {
+ setError("Not connected to machine");
+ return;
+ }
+
+ try {
+ setError(null);
+ setUploadProgress(0);
+ const uuid = await service.uploadPattern(penData, (progress) => {
+ setUploadProgress(progress);
+ });
+ setUploadProgress(100);
+
+ // Cache the pattern with its UUID
+ const uuidStr = uuidToString(uuid);
+ PatternCacheService.savePattern(uuidStr, pesData, fileName);
+ console.log("[Cache] Saved pattern:", fileName, "with UUID:", uuidStr);
+
+ // Clear resume state since we just uploaded
+ setResumeAvailable(false);
+ setResumeFileName(null);
+
+ // Refresh status and pattern info after upload
+ await refreshStatus();
+ await refreshPatternInfo();
+ } catch (err) {
+ setError(
+ err instanceof Error ? err.message : "Failed to upload pattern",
+ );
+ }
+ },
+ [service, isConnected, refreshStatus, refreshPatternInfo],
+ );
+
+ const startMaskTrace = useCallback(async () => {
+ if (!isConnected) return;
+
+ try {
+ setError(null);
+ await service.startMaskTrace();
+ await refreshStatus();
+ } catch (err) {
+ setError(
+ err instanceof Error ? err.message : "Failed to start mask trace",
+ );
+ }
+ }, [service, isConnected, refreshStatus]);
+
+ const startSewing = useCallback(async () => {
+ if (!isConnected) return;
+
+ try {
+ setError(null);
+ await service.startSewing();
+ await refreshStatus();
+ } catch (err) {
+ setError(err instanceof Error ? err.message : "Failed to start sewing");
+ }
+ }, [service, isConnected, refreshStatus]);
+
+ const resumeSewing = useCallback(async () => {
+ if (!isConnected) return;
+
+ try {
+ setError(null);
+ await service.resumeSewing();
+ await refreshStatus();
+ } catch (err) {
+ setError(err instanceof Error ? err.message : "Failed to resume sewing");
+ }
+ }, [service, isConnected, refreshStatus]);
+
+ const deletePattern = useCallback(async () => {
+ if (!isConnected) return;
+
+ try {
+ setError(null);
+ await service.deletePattern();
+ setPatternInfo(null);
+ setSewingProgress(null);
+ await refreshStatus();
+ } catch (err) {
+ setError(err instanceof Error ? err.message : "Failed to delete pattern");
+ }
+ }, [service, isConnected, refreshStatus]);
+
+ // Periodic status monitoring when connected
+ useEffect(() => {
+ if (!isConnected) {
+ return;
+ }
+
+ // Determine polling interval based on machine status
+ let pollInterval = 2000; // Default: 2 seconds for idle states
+
+ // Fast polling for active states
+ if (
+ machineStatus === MachineStatus.SEWING ||
+ machineStatus === MachineStatus.MASK_TRACING ||
+ machineStatus === MachineStatus.SEWING_DATA_RECEIVE
+ ) {
+ pollInterval = 500; // 500ms for active operations
+ } else if (
+ machineStatus === MachineStatus.COLOR_CHANGE_WAIT ||
+ machineStatus === MachineStatus.MASK_TRACE_LOCK_WAIT ||
+ machineStatus === MachineStatus.SEWING_WAIT
+ ) {
+ pollInterval = 1000; // 1 second for waiting states
+ }
+
+ const interval = setInterval(async () => {
+ await refreshStatus();
+
+ // Also refresh progress during sewing
+ if (machineStatus === MachineStatus.SEWING) {
+ await refreshProgress();
+ }
+ }, pollInterval);
+
+ return () => clearInterval(interval);
+ }, [isConnected, machineStatus, refreshStatus, refreshProgress]);
+
+ // Refresh pattern info when status changes to SEWING_WAIT
+ // (indicates pattern was just uploaded or is ready)
+ useEffect(() => {
+ if (!isConnected) return;
+
+ if (machineStatus === MachineStatus.SEWING_WAIT && !patternInfo) {
+ refreshPatternInfo();
+ }
+ }, [isConnected, machineStatus, patternInfo, refreshPatternInfo]);
+
+ return {
+ isConnected,
+ machineInfo,
+ machineStatus,
+ machineStatusName: MachineStatusNames[machineStatus] || "Unknown",
+ machineError,
+ patternInfo,
+ sewingProgress,
+ uploadProgress,
+ error,
+ isPolling,
+ resumeAvailable,
+ resumeFileName,
+ resumedPattern,
+ connect,
+ disconnect,
+ refreshStatus,
+ refreshPatternInfo,
+ uploadPattern,
+ startMaskTrace,
+ startSewing,
+ resumeSewing,
+ deletePattern,
+ checkResume,
+ loadCachedPattern,
+ };
+}
diff --git a/src/index.css b/src/index.css
new file mode 100644
index 0000000..ec2585e
--- /dev/null
+++ b/src/index.css
@@ -0,0 +1,13 @@
+body {
+ margin: 0;
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
+ 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
+ sans-serif;
+ -webkit-font-smoothing: antialiased;
+ -moz-osx-font-smoothing: grayscale;
+}
+
+code {
+ font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New',
+ monospace;
+}
diff --git a/src/main.tsx b/src/main.tsx
new file mode 100644
index 0000000..bef5202
--- /dev/null
+++ b/src/main.tsx
@@ -0,0 +1,10 @@
+import { StrictMode } from 'react'
+import { createRoot } from 'react-dom/client'
+import './index.css'
+import App from './App.tsx'
+
+createRoot(document.getElementById('root')!).render(
+ <StrictMode>
+ <App />
+ </StrictMode>,
+)
diff --git a/src/services/BrotherPP1Service.ts b/src/services/BrotherPP1Service.ts
new file mode 100644
index 0000000..6bfe4ba
--- /dev/null
+++ b/src/services/BrotherPP1Service.ts
@@ -0,0 +1,452 @@
+import type {
+ MachineInfo,
+ PatternInfo,
+ SewingProgress,
+} from "../types/machine";
+import { MachineStatus } from "../types/machine";
+
+// BLE Service and Characteristic UUIDs
+const SERVICE_UUID = "a76eb9e0-f3ac-4990-84cf-3a94d2426b2b";
+const WRITE_CHAR_UUID = "a76eb9e2-f3ac-4990-84cf-3a94d2426b2b";
+const READ_CHAR_UUID = "a76eb9e1-f3ac-4990-84cf-3a94d2426b2b";
+
+// Command IDs (big-endian)
+const Commands = {
+ MACHINE_INFO: 0x0000,
+ MACHINE_STATE: 0x0001,
+ SERVICE_COUNT: 0x0100,
+ PATTERN_UUID_REQUEST: 0x0702,
+ MASK_TRACE: 0x0704,
+ LAYOUT_SEND: 0x0705,
+ EMB_SEWING_INFO_REQUEST: 0x0706,
+ PATTERN_SEWING_INFO: 0x0707,
+ EMB_SEWING_DATA_DELETE: 0x0708,
+ NEEDLE_MODE_INSTRUCTIONS: 0x0709,
+ EMB_UUID_SEND: 0x070a,
+ RESUME_FLAG_REQUEST: 0x070b,
+ RESUME: 0x070c,
+ START_SEWING: 0x070e,
+ MASK_TRACE_1: 0x0710,
+ EMB_ORG_POINT: 0x0800,
+ MACHINE_SETTING_INFO: 0x0c02,
+ SEND_DATA_INFO: 0x1200,
+ SEND_DATA: 0x1201,
+ CLEAR_ERROR: 0x1300,
+};
+
+export class BrotherPP1Service {
+ private device: BluetoothDevice | null = null;
+ private server: BluetoothRemoteGATTServer | null = null;
+ private writeCharacteristic: BluetoothRemoteGATTCharacteristic | null = null;
+ private readCharacteristic: BluetoothRemoteGATTCharacteristic | null = null;
+ private commandQueue: Array<() => Promise<void>> = [];
+ private isProcessingQueue = false;
+
+ async connect(): Promise<void> {
+ this.device = await navigator.bluetooth.requestDevice({
+ filters: [{ services: [SERVICE_UUID] }],
+ });
+
+ if (!this.device.gatt) {
+ throw new Error("GATT not available");
+ }
+ console.log("Connecting");
+ this.server = await this.device.gatt.connect();
+ console.log("Connected");
+ const service = await this.server.getPrimaryService(SERVICE_UUID);
+ console.log("Got primary service");
+
+ this.writeCharacteristic = await service.getCharacteristic(WRITE_CHAR_UUID);
+ this.readCharacteristic = await service.getCharacteristic(READ_CHAR_UUID);
+
+ console.log("Connected to Brother PP1 machine");
+
+ console.log("Send dummy command");
+ try {
+ await this.getMachineInfo();
+ console.log("Dummy command success");
+ } catch (e) {
+ console.log(e);
+ }
+ }
+
+ async disconnect(): Promise<void> {
+ // Clear any pending commands
+ this.commandQueue = [];
+ this.isProcessingQueue = false;
+
+ if (this.server) {
+ this.server.disconnect();
+ }
+ this.device = null;
+ this.server = null;
+ this.writeCharacteristic = null;
+ this.readCharacteristic = null;
+ }
+
+ isConnected(): boolean {
+ return this.server?.connected ?? false;
+ }
+
+ /**
+ * Process the command queue sequentially
+ */
+ private async processQueue(): Promise<void> {
+ if (this.isProcessingQueue || this.commandQueue.length === 0) {
+ return;
+ }
+
+ this.isProcessingQueue = true;
+
+ while (this.commandQueue.length > 0) {
+ const command = this.commandQueue.shift();
+ if (command) {
+ try {
+ await command();
+ } catch (err) {
+ console.error("Command queue error:", err);
+ // Continue processing queue even if one command fails
+ }
+ }
+ }
+
+ this.isProcessingQueue = false;
+ }
+
+ /**
+ * Enqueue a Bluetooth command to be executed sequentially
+ */
+ private async enqueueCommand<T>(operation: () => Promise<T>): Promise<T> {
+ return new Promise<T>((resolve, reject) => {
+ this.commandQueue.push(async () => {
+ try {
+ const result = await operation();
+ resolve(result);
+ } catch (err) {
+ reject(err);
+ }
+ });
+
+ // Start processing the queue
+ this.processQueue();
+ });
+ }
+
+ private async sendCommand(
+ cmdId: number,
+ data: Uint8Array = new Uint8Array(),
+ ): Promise<Uint8Array> {
+ // Enqueue the command to ensure sequential execution
+ return this.enqueueCommand(async () => {
+ if (!this.writeCharacteristic || !this.readCharacteristic) {
+ throw new Error("Not connected");
+ }
+
+ // Build command with big-endian command ID
+ const command = new Uint8Array(2 + data.length);
+ command[0] = (cmdId >> 8) & 0xff; // High byte
+ command[1] = cmdId & 0xff; // Low byte
+ command.set(data, 2);
+
+ console.log(
+ "Sending command:",
+ Array.from(command)
+ .map((b) => b.toString(16).padStart(2, "0"))
+ .join(" "),
+ );
+ console.log("Sending command");
+ // Write command and immediately read response
+ await this.writeCharacteristic.writeValueWithoutResponse(command);
+
+ console.log("delay");
+ // Small delay to ensure response is ready
+ await new Promise((resolve) => setTimeout(resolve, 50));
+ console.log("reading response");
+
+ const responseData = await this.readCharacteristic.readValue();
+ const response = new Uint8Array(responseData.buffer);
+
+ console.log(
+ "Received response:",
+ Array.from(response)
+ .map((b) => b.toString(16).padStart(2, "0"))
+ .join(" "),
+ );
+
+ return response;
+ });
+ }
+
+ async getMachineInfo(): Promise<MachineInfo> {
+ const response = await this.sendCommand(Commands.MACHINE_INFO);
+
+ // Skip 2-byte command header
+ const data = response.slice(2);
+
+ const decoder = new TextDecoder("ascii");
+ const serialNumber = decoder.decode(data.slice(2, 11)).replace(/\0/g, "");
+ const modelCode = decoder.decode(data.slice(39, 50)).replace(/\0/g, "");
+
+ // Software version (big-endian int16)
+ const swVersion = (data[0] << 8) | data[1];
+
+ // BT version (big-endian int16)
+ const btVersion = (data[24] << 8) | data[25];
+
+ // Max dimensions (little-endian int16)
+ const maxWidth = data[29] | (data[30] << 8);
+ const maxHeight = data[31] | (data[32] << 8);
+
+ // MAC address
+ const macAddress = Array.from(data.slice(16, 22))
+ .map((b) => b.toString(16).padStart(2, "0"))
+ .join(":")
+ .toUpperCase();
+
+ return {
+ serialNumber,
+ modelNumber: modelCode,
+ softwareVersion: `${(swVersion / 100).toFixed(2)}.${data[35]}`,
+ bluetoothVersion: btVersion,
+ maxWidth,
+ maxHeight,
+ macAddress,
+ };
+ }
+
+ async getMachineState(): Promise<{ status: MachineStatus; error: number }> {
+ const response = await this.sendCommand(Commands.MACHINE_STATE);
+
+ return {
+ status: response[2] as MachineStatus,
+ error: response[4],
+ };
+ }
+
+ async getPatternInfo(): Promise<PatternInfo> {
+ const response = await this.sendCommand(Commands.EMB_SEWING_INFO_REQUEST);
+ const data = response.slice(2);
+
+ const readInt16LE = (offset: number) =>
+ data[offset] | (data[offset + 1] << 8);
+ const readUInt16LE = (offset: number) =>
+ data[offset] | (data[offset + 1] << 8);
+
+ return {
+ boundLeft: readInt16LE(0),
+ boundTop: readInt16LE(2),
+ boundRight: readInt16LE(4),
+ boundBottom: readInt16LE(6),
+ totalTime: readUInt16LE(8),
+ totalStitches: readUInt16LE(10),
+ speed: readUInt16LE(12),
+ };
+ }
+
+ async getSewingProgress(): Promise<SewingProgress> {
+ const response = await this.sendCommand(Commands.PATTERN_SEWING_INFO);
+ const data = response.slice(2);
+
+ const readInt16LE = (offset: number) => {
+ const value = data[offset] | (data[offset + 1] << 8);
+ // Convert to signed 16-bit integer
+ return value > 0x7fff ? value - 0x10000 : value;
+ };
+ const readUInt16LE = (offset: number) =>
+ data[offset] | (data[offset + 1] << 8);
+
+ return {
+ currentStitch: readUInt16LE(0),
+ currentTime: readInt16LE(2),
+ stopTime: readInt16LE(4),
+ positionX: readInt16LE(6),
+ positionY: readInt16LE(8),
+ };
+ }
+
+ async deletePattern(): Promise<void> {
+ await this.sendCommand(Commands.EMB_SEWING_DATA_DELETE);
+ }
+
+ async sendDataInfo(length: number, checksum: number): Promise<void> {
+ const payload = new Uint8Array(7);
+ payload[0] = 0x03; // Type
+
+ // Length (little-endian uint32)
+ payload[1] = length & 0xff;
+ payload[2] = (length >> 8) & 0xff;
+ payload[3] = (length >> 16) & 0xff;
+ payload[4] = (length >> 24) & 0xff;
+
+ // Checksum (little-endian uint16)
+ payload[5] = checksum & 0xff;
+ payload[6] = (checksum >> 8) & 0xff;
+
+ const response = await this.sendCommand(Commands.SEND_DATA_INFO, payload);
+
+ if (response[2] !== 0x00) {
+ throw new Error("Data info rejected");
+ }
+ }
+
+ async sendDataChunk(offset: number, data: Uint8Array): Promise<boolean> {
+ const checksum = data.reduce((sum, byte) => (sum + byte) & 0xff, 0);
+
+ const payload = new Uint8Array(4 + data.length + 1);
+
+ // Offset (little-endian uint32)
+ payload[0] = offset & 0xff;
+ payload[1] = (offset >> 8) & 0xff;
+ payload[2] = (offset >> 16) & 0xff;
+ payload[3] = (offset >> 24) & 0xff;
+
+ payload.set(data, 4);
+ payload[4 + data.length] = checksum;
+
+ const response = await this.sendCommand(Commands.SEND_DATA, payload);
+
+ // 0x00 = complete, 0x02 = continue
+ return response[2] === 0x00;
+ }
+
+ async sendUUID(uuid: Uint8Array): Promise<void> {
+ const response = await this.sendCommand(Commands.EMB_UUID_SEND, uuid);
+
+ if (response[2] !== 0x00) {
+ throw new Error("UUID rejected");
+ }
+ }
+
+ async sendLayout(
+ moveX: number,
+ moveY: number,
+ sizeX: number,
+ sizeY: number,
+ rotate: number,
+ flip: number,
+ frame: number,
+ ): Promise<void> {
+ const payload = new Uint8Array(12);
+
+ const writeInt16LE = (offset: number, value: number) => {
+ payload[offset] = value & 0xff;
+ payload[offset + 1] = (value >> 8) & 0xff;
+ };
+
+ writeInt16LE(0, moveX);
+ writeInt16LE(2, moveY);
+ writeInt16LE(4, sizeX);
+ writeInt16LE(6, sizeY);
+ writeInt16LE(8, rotate);
+ payload[10] = flip;
+ payload[11] = frame;
+
+ await this.sendCommand(Commands.LAYOUT_SEND, payload);
+ }
+
+ async startMaskTrace(): Promise<void> {
+ const payload = new Uint8Array([0x01]);
+ await this.sendCommand(Commands.MASK_TRACE, payload);
+ }
+
+ async startSewing(): Promise<void> {
+ await this.sendCommand(Commands.START_SEWING);
+ }
+
+ async resumeSewing(): Promise<void> {
+ // Resume uses the same START_SEWING command as initial start
+ // The machine tracks current position and resumes from there
+ await this.sendCommand(Commands.START_SEWING);
+ }
+
+ async uploadPattern(
+ data: Uint8Array,
+ onProgress?: (progress: number) => void,
+ ): Promise<Uint8Array> {
+ // Calculate checksum
+ const checksum = data.reduce((sum, byte) => sum + byte, 0) & 0xffff;
+
+ // Delete existing pattern
+ await this.deletePattern();
+
+ // Send data info
+ await this.sendDataInfo(data.length, checksum);
+
+ // Send data in chunks (max chunk size ~500 bytes to be safe with BLE MTU)
+ const chunkSize = 500;
+ let offset = 0;
+
+ while (offset < data.length) {
+ const chunk = data.slice(
+ offset,
+ Math.min(offset + chunkSize, data.length),
+ );
+ const isComplete = await this.sendDataChunk(offset, chunk);
+
+ offset += chunk.length;
+
+ if (onProgress) {
+ onProgress((offset / data.length) * 100);
+ }
+
+ if (isComplete) {
+ break;
+ }
+
+ // Small delay between chunks
+ await new Promise((resolve) => setTimeout(resolve, 10));
+ }
+
+ // Generate random UUID
+ const uuid = crypto.getRandomValues(new Uint8Array(16));
+ await this.sendUUID(uuid);
+
+ // Send default layout (no transformation)
+ await this.sendLayout(0, 0, 0, 0, 0, 0, 0);
+
+ console.log(
+ "Pattern uploaded successfully with UUID:",
+ Array.from(uuid)
+ .map((b) => b.toString(16).padStart(2, "0"))
+ .join(""),
+ );
+
+ // Return UUID for caching
+ return uuid;
+ }
+
+ /**
+ * Request the UUID of the pattern currently loaded on the machine
+ */
+ async getPatternUUID(): Promise<Uint8Array | null> {
+ try {
+ const response = await this.sendCommand(Commands.PATTERN_UUID_REQUEST);
+
+ // Response format: [cmd_high, cmd_low, uuid_bytes...]
+ // UUID starts at index 2 (16 bytes)
+ if (response.length < 18) {
+ // Not enough data for UUID
+ console.log(
+ "[BrotherPP1] Response too short for UUID:",
+ response.length,
+ );
+ return null;
+ }
+
+ // Extract UUID (16 bytes starting at index 2)
+ const uuid = response.slice(2, 18);
+
+ // Check if UUID is all zeros (no pattern loaded)
+ const allZeros = uuid.every((byte) => byte === 0);
+ if (allZeros) {
+ console.log("[BrotherPP1] UUID is all zeros, no pattern loaded");
+ return null;
+ }
+
+ return uuid;
+ } catch (err) {
+ console.error("[BrotherPP1] Failed to get pattern UUID:", err);
+ return null;
+ }
+ }
+}
diff --git a/src/services/PatternCacheService.ts b/src/services/PatternCacheService.ts
new file mode 100644
index 0000000..b12f5b6
--- /dev/null
+++ b/src/services/PatternCacheService.ts
@@ -0,0 +1,155 @@
+import type { PesPatternData } from '../utils/pystitchConverter';
+
+interface CachedPattern {
+ uuid: string;
+ pesData: PesPatternData;
+ fileName: string;
+ timestamp: number;
+}
+
+const CACHE_KEY = 'brother_pattern_cache';
+
+/**
+ * Convert UUID Uint8Array to hex string
+ */
+export function uuidToString(uuid: Uint8Array): string {
+ return Array.from(uuid).map(b => b.toString(16).padStart(2, '0')).join('');
+}
+
+/**
+ * Convert hex string to UUID Uint8Array
+ */
+export function stringToUuid(str: string): Uint8Array {
+ const bytes = new Uint8Array(16);
+ for (let i = 0; i < 16; i++) {
+ bytes[i] = parseInt(str.substr(i * 2, 2), 16);
+ }
+ return bytes;
+}
+
+export class PatternCacheService {
+ /**
+ * Save pattern to local storage with its UUID
+ */
+ static savePattern(
+ uuid: string,
+ pesData: PesPatternData,
+ fileName: string
+ ): void {
+ try {
+ // Convert penData Uint8Array to array for JSON serialization
+ const pesDataWithArrayPenData = {
+ ...pesData,
+ penData: Array.from(pesData.penData) as any,
+ };
+
+ const cached: CachedPattern = {
+ uuid,
+ pesData: pesDataWithArrayPenData,
+ fileName,
+ timestamp: Date.now(),
+ };
+
+ localStorage.setItem(CACHE_KEY, JSON.stringify(cached));
+ console.log('[PatternCache] Saved pattern:', fileName, 'UUID:', uuid);
+ } catch (err) {
+ console.error('[PatternCache] Failed to save pattern:', err);
+ // If quota exceeded, clear and try again
+ if (err instanceof Error && err.name === 'QuotaExceededError') {
+ this.clearCache();
+ }
+ }
+ }
+
+ /**
+ * Get cached pattern by UUID
+ */
+ static getPatternByUUID(uuid: string): CachedPattern | null {
+ try {
+ const cached = localStorage.getItem(CACHE_KEY);
+ if (!cached) {
+ return null;
+ }
+
+ const pattern: CachedPattern = JSON.parse(cached);
+
+ // Check if UUID matches
+ if (pattern.uuid !== uuid) {
+ console.log('[PatternCache] UUID mismatch. Cached:', pattern.uuid, 'Requested:', uuid);
+ return null;
+ }
+
+ // Restore Uint8Array from array inside pesData
+ if (Array.isArray(pattern.pesData.penData)) {
+ pattern.pesData.penData = new Uint8Array(pattern.pesData.penData);
+ }
+
+ console.log('[PatternCache] Found cached pattern:', pattern.fileName, 'UUID:', uuid);
+ return pattern;
+ } catch (err) {
+ console.error('[PatternCache] Failed to retrieve pattern:', err);
+ return null;
+ }
+ }
+
+ /**
+ * Get the most recent cached pattern (regardless of UUID)
+ */
+ static getMostRecentPattern(): CachedPattern | null {
+ try {
+ const cached = localStorage.getItem(CACHE_KEY);
+ if (!cached) {
+ return null;
+ }
+
+ const pattern: CachedPattern = JSON.parse(cached);
+
+ // Restore Uint8Array from array inside pesData
+ if (Array.isArray(pattern.pesData.penData)) {
+ pattern.pesData.penData = new Uint8Array(pattern.pesData.penData);
+ }
+
+ return pattern;
+ } catch (err) {
+ console.error('[PatternCache] Failed to retrieve pattern:', err);
+ return null;
+ }
+ }
+
+ /**
+ * Check if a pattern with the given UUID exists in cache
+ */
+ static hasPattern(uuid: string): boolean {
+ const pattern = this.getMostRecentPattern();
+ return pattern?.uuid === uuid;
+ }
+
+ /**
+ * Clear the pattern cache
+ */
+ static clearCache(): void {
+ try {
+ localStorage.removeItem(CACHE_KEY);
+ console.log('[PatternCache] Cache cleared');
+ } catch (err) {
+ console.error('[PatternCache] Failed to clear cache:', err);
+ }
+ }
+
+ /**
+ * Get cache info for debugging
+ */
+ static getCacheInfo(): { hasCache: boolean; fileName?: string; uuid?: string; age?: number } {
+ const pattern = this.getMostRecentPattern();
+ if (!pattern) {
+ return { hasCache: false };
+ }
+
+ return {
+ hasCache: true,
+ fileName: pattern.fileName,
+ uuid: pattern.uuid,
+ age: Date.now() - pattern.timestamp,
+ };
+ }
+}
diff --git a/src/types/machine.ts b/src/types/machine.ts
new file mode 100644
index 0000000..81777c5
--- /dev/null
+++ b/src/types/machine.ts
@@ -0,0 +1,103 @@
+// Brother PP1 Machine Types
+
+export const MachineStatus = {
+ Initial: 0x00,
+ LowerThread: 0x01,
+ IDLE: 0x10,
+ SEWING_WAIT: 0x11,
+ SEWING_DATA_RECEIVE: 0x12,
+ MASK_TRACE_LOCK_WAIT: 0x20,
+ MASK_TRACING: 0x21,
+ MASK_TRACE_COMPLETE: 0x22,
+ SEWING: 0x30,
+ SEWING_COMPLETE: 0x31,
+ SEWING_INTERRUPTION: 0x32,
+ COLOR_CHANGE_WAIT: 0x40,
+ PAUSE: 0x41,
+ STOP: 0x42,
+ HOOP_AVOIDANCE: 0x50,
+ HOOP_AVOIDANCEING: 0x51,
+ RL_RECEIVING: 0x60,
+ RL_RECEIVED: 0x61,
+ None: 0xDD,
+ TryConnecting: 0xFF,
+} as const;
+
+export type MachineStatus = typeof MachineStatus[keyof typeof MachineStatus];
+
+export const MachineStatusNames: Record<MachineStatus, string> = {
+ [MachineStatus.Initial]: 'Initial',
+ [MachineStatus.LowerThread]: 'Lower Thread',
+ [MachineStatus.IDLE]: 'Idle',
+ [MachineStatus.SEWING_WAIT]: 'Ready to Sew',
+ [MachineStatus.SEWING_DATA_RECEIVE]: 'Receiving Data',
+ [MachineStatus.MASK_TRACE_LOCK_WAIT]: 'Waiting for Mask Trace',
+ [MachineStatus.MASK_TRACING]: 'Mask Tracing',
+ [MachineStatus.MASK_TRACE_COMPLETE]: 'Mask Trace Complete',
+ [MachineStatus.SEWING]: 'Sewing',
+ [MachineStatus.SEWING_COMPLETE]: 'Complete',
+ [MachineStatus.SEWING_INTERRUPTION]: 'Interrupted',
+ [MachineStatus.COLOR_CHANGE_WAIT]: 'Waiting for Color Change',
+ [MachineStatus.PAUSE]: 'Paused',
+ [MachineStatus.STOP]: 'Stopped',
+ [MachineStatus.HOOP_AVOIDANCE]: 'Hoop Avoidance',
+ [MachineStatus.HOOP_AVOIDANCEING]: 'Hoop Avoidance In Progress',
+ [MachineStatus.RL_RECEIVING]: 'RL Receiving',
+ [MachineStatus.RL_RECEIVED]: 'RL Received',
+ [MachineStatus.None]: 'None',
+ [MachineStatus.TryConnecting]: 'Connecting',
+};
+
+export interface MachineInfo {
+ serialNumber: string;
+ modelNumber: string;
+ softwareVersion: string;
+ bluetoothVersion: number;
+ maxWidth: number; // in 0.1mm units
+ maxHeight: number; // in 0.1mm units
+ macAddress: string;
+}
+
+export interface PatternInfo {
+ totalStitches: number;
+ totalTime: number; // seconds
+ speed: number; // stitches per minute
+ boundLeft: number;
+ boundTop: number;
+ boundRight: number;
+ boundBottom: number;
+}
+
+export interface SewingProgress {
+ currentStitch: number;
+ currentTime: number; // seconds
+ stopTime: number;
+ positionX: number; // in 0.1mm units
+ positionY: number; // in 0.1mm units
+}
+
+export interface PenStitch {
+ x: number;
+ y: number;
+ flags: number;
+ isJump: boolean;
+}
+
+export interface PenColorBlock {
+ startStitch: number;
+ endStitch: number;
+ colorIndex: number;
+}
+
+export interface PenData {
+ stitches: PenStitch[];
+ colorBlocks: PenColorBlock[];
+ totalStitches: number;
+ colorCount: number;
+ bounds: {
+ minX: number;
+ maxX: number;
+ minY: number;
+ maxY: number;
+ };
+}
diff --git a/src/utils/errorCodeHelpers.ts b/src/utils/errorCodeHelpers.ts
new file mode 100644
index 0000000..989be88
--- /dev/null
+++ b/src/utils/errorCodeHelpers.ts
@@ -0,0 +1,100 @@
+/**
+ * Brother PP1 Protocol Error Codes
+ * Based on App/Asura.Core/Models/SewingMachineError.cs
+ */
+
+export enum SewingMachineError {
+ NeedlePositionError = 0x00,
+ SafetyError = 0x01,
+ LowerThreadSafetyError = 0x02,
+ LowerThreadFreeError = 0x03,
+ RestartError10 = 0x10,
+ RestartError11 = 0x11,
+ RestartError12 = 0x12,
+ RestartError13 = 0x13,
+ RestartError14 = 0x14,
+ RestartError15 = 0x15,
+ RestartError16 = 0x16,
+ RestartError17 = 0x17,
+ RestartError18 = 0x18,
+ RestartError19 = 0x19,
+ RestartError1A = 0x1A,
+ RestartError1B = 0x1B,
+ RestartError1C = 0x1C,
+ NeedlePlateError = 0x20,
+ ThreadLeverError = 0x21,
+ UpperThreadError = 0x60,
+ LowerThreadError = 0x61,
+ UpperThreadSewingStartError = 0x62,
+ PRWiperError = 0x63,
+ HoopError = 0x70,
+ NoHoopError = 0x71,
+ InitialHoopError = 0x72,
+ RegularInspectionError = 0x80,
+ Setting = 0x98,
+ None = 0xDD,
+ Unknown = 0xEE,
+ OtherError = 0xFF,
+}
+
+/**
+ * Human-readable error messages
+ */
+const ERROR_MESSAGES: Record<number, string> = {
+ [SewingMachineError.NeedlePositionError]: 'Needle Position Error',
+ [SewingMachineError.SafetyError]: 'Safety Error',
+ [SewingMachineError.LowerThreadSafetyError]: 'Lower Thread Safety Error',
+ [SewingMachineError.LowerThreadFreeError]: 'Lower Thread Free Error',
+ [SewingMachineError.RestartError10]: 'Restart Required (0x10)',
+ [SewingMachineError.RestartError11]: 'Restart Required (0x11)',
+ [SewingMachineError.RestartError12]: 'Restart Required (0x12)',
+ [SewingMachineError.RestartError13]: 'Restart Required (0x13)',
+ [SewingMachineError.RestartError14]: 'Restart Required (0x14)',
+ [SewingMachineError.RestartError15]: 'Restart Required (0x15)',
+ [SewingMachineError.RestartError16]: 'Restart Required (0x16)',
+ [SewingMachineError.RestartError17]: 'Restart Required (0x17)',
+ [SewingMachineError.RestartError18]: 'Restart Required (0x18)',
+ [SewingMachineError.RestartError19]: 'Restart Required (0x19)',
+ [SewingMachineError.RestartError1A]: 'Restart Required (0x1A)',
+ [SewingMachineError.RestartError1B]: 'Restart Required (0x1B)',
+ [SewingMachineError.RestartError1C]: 'Restart Required (0x1C)',
+ [SewingMachineError.NeedlePlateError]: 'Needle Plate Error',
+ [SewingMachineError.ThreadLeverError]: 'Thread Lever Error',
+ [SewingMachineError.UpperThreadError]: 'Upper Thread Error',
+ [SewingMachineError.LowerThreadError]: 'Lower Thread Error',
+ [SewingMachineError.UpperThreadSewingStartError]: 'Upper Thread Error at Sewing Start',
+ [SewingMachineError.PRWiperError]: 'PR Wiper Error',
+ [SewingMachineError.HoopError]: 'Hoop Error',
+ [SewingMachineError.NoHoopError]: 'No Hoop Detected',
+ [SewingMachineError.InitialHoopError]: 'Initial Hoop Error',
+ [SewingMachineError.RegularInspectionError]: 'Regular Inspection Required',
+ [SewingMachineError.Setting]: 'Settings Error',
+ [SewingMachineError.Unknown]: 'Unknown Error',
+ [SewingMachineError.OtherError]: 'Other Error',
+};
+
+/**
+ * Get human-readable error message for an error code
+ */
+export function getErrorMessage(errorCode: number): string | null {
+ // 0xDD (221) is the default "no error" value
+ if (errorCode === SewingMachineError.None) {
+ return null; // No error to display
+ }
+
+ // Look up known error message
+ const message = ERROR_MESSAGES[errorCode];
+ if (message) {
+ return message;
+ }
+
+ // Unknown error code
+ return `Machine Error ${errorCode} (0x${errorCode.toString(16).toUpperCase().padStart(2, '0')})`;
+}
+
+/**
+ * Check if error code represents an actual error condition
+ */
+export function hasError(errorCode: number): boolean {
+ return errorCode !== SewingMachineError.None;
+}
diff --git a/src/utils/machineStateHelpers.ts b/src/utils/machineStateHelpers.ts
new file mode 100644
index 0000000..b1e8fc2
--- /dev/null
+++ b/src/utils/machineStateHelpers.ts
@@ -0,0 +1,184 @@
+import { MachineStatus } from '../types/machine';
+
+/**
+ * Machine state categories for safety logic
+ */
+export const MachineStateCategory = {
+ IDLE: 'idle',
+ ACTIVE: 'active',
+ WAITING: 'waiting',
+ COMPLETE: 'complete',
+ INTERRUPTED: 'interrupted',
+ ERROR: 'error',
+} as const;
+
+export type MachineStateCategoryType = typeof MachineStateCategory[keyof typeof MachineStateCategory];
+
+/**
+ * Categorize a machine status into a semantic safety category
+ */
+export function getMachineStateCategory(status: MachineStatus): MachineStateCategoryType {
+ switch (status) {
+ // IDLE states - safe to perform any action
+ case MachineStatus.IDLE:
+ case MachineStatus.SEWING_WAIT:
+ case MachineStatus.Initial:
+ case MachineStatus.LowerThread:
+ return MachineStateCategory.IDLE;
+
+ // ACTIVE states - operation in progress, dangerous to interrupt
+ case MachineStatus.SEWING:
+ case MachineStatus.MASK_TRACING:
+ case MachineStatus.SEWING_DATA_RECEIVE:
+ case MachineStatus.HOOP_AVOIDANCEING:
+ return MachineStateCategory.ACTIVE;
+
+ // WAITING states - waiting for user/machine action
+ case MachineStatus.COLOR_CHANGE_WAIT:
+ case MachineStatus.MASK_TRACE_LOCK_WAIT:
+ case MachineStatus.HOOP_AVOIDANCE:
+ return MachineStateCategory.WAITING;
+
+ // COMPLETE states - operation finished
+ case MachineStatus.SEWING_COMPLETE:
+ case MachineStatus.MASK_TRACE_COMPLETE:
+ case MachineStatus.RL_RECEIVED:
+ return MachineStateCategory.COMPLETE;
+
+ // INTERRUPTED states - operation paused/stopped
+ case MachineStatus.PAUSE:
+ case MachineStatus.STOP:
+ case MachineStatus.SEWING_INTERRUPTION:
+ return MachineStateCategory.INTERRUPTED;
+
+ // ERROR/UNKNOWN states
+ case MachineStatus.None:
+ case MachineStatus.TryConnecting:
+ case MachineStatus.RL_RECEIVING:
+ default:
+ return MachineStateCategory.ERROR;
+ }
+}
+
+/**
+ * Determines if the pattern can be safely deleted in the current state.
+ * Prevents deletion during active operations (SEWING, MASK_TRACING, etc.)
+ */
+export function canDeletePattern(status: MachineStatus): boolean {
+ const category = getMachineStateCategory(status);
+ // Can only delete in IDLE or COMPLETE states, never during ACTIVE operations
+ return category === MachineStateCategory.IDLE ||
+ category === MachineStateCategory.COMPLETE;
+}
+
+/**
+ * Determines if a pattern can be safely uploaded in the current state.
+ * Only allow uploads when machine is idle.
+ */
+export function canUploadPattern(status: MachineStatus): boolean {
+ const category = getMachineStateCategory(status);
+ // Can only upload in IDLE state
+ return category === MachineStateCategory.IDLE;
+}
+
+/**
+ * Determines if sewing can be started in the current state.
+ * Allows starting from ready state or resuming from interrupted states.
+ */
+export function canStartSewing(status: MachineStatus): boolean {
+ // Only in specific ready states
+ return status === MachineStatus.SEWING_WAIT ||
+ status === MachineStatus.PAUSE ||
+ status === MachineStatus.STOP ||
+ status === MachineStatus.SEWING_INTERRUPTION;
+}
+
+/**
+ * Determines if mask trace can be started in the current state.
+ */
+export function canStartMaskTrace(status: MachineStatus): boolean {
+ // Only when ready or after previous trace
+ return status === MachineStatus.SEWING_WAIT ||
+ status === MachineStatus.MASK_TRACE_COMPLETE;
+}
+
+/**
+ * Determines if sewing can be resumed in the current state.
+ * Only for interrupted operations (PAUSE, STOP, SEWING_INTERRUPTION).
+ */
+export function canResumeSewing(status: MachineStatus): boolean {
+ // Only in interrupted states
+ const category = getMachineStateCategory(status);
+ return category === MachineStateCategory.INTERRUPTED;
+}
+
+/**
+ * Determines if disconnect should show a confirmation dialog.
+ * Confirms if disconnecting during active operation or while waiting.
+ */
+export function shouldConfirmDisconnect(status: MachineStatus): boolean {
+ const category = getMachineStateCategory(status);
+ // Confirm if disconnecting during active operation or waiting for action
+ return category === MachineStateCategory.ACTIVE ||
+ category === MachineStateCategory.WAITING;
+}
+
+/**
+ * Visual information for a machine state
+ */
+export interface StateVisualInfo {
+ color: string;
+ icon: string;
+ label: string;
+ description: string;
+}
+
+/**
+ * Get visual styling information for a machine state.
+ * Returns color, icon, label, and description for UI display.
+ */
+export function getStateVisualInfo(status: MachineStatus): StateVisualInfo {
+ const category = getMachineStateCategory(status);
+
+ // Map state category to visual properties
+ const visualMap: Record<MachineStateCategoryType, StateVisualInfo> = {
+ [MachineStateCategory.IDLE]: {
+ color: 'info',
+ icon: '⭕',
+ label: 'Ready',
+ description: 'Machine is idle and ready for operations'
+ },
+ [MachineStateCategory.ACTIVE]: {
+ color: 'warning',
+ icon: '▶️',
+ label: 'Active',
+ description: 'Operation in progress - do not interrupt'
+ },
+ [MachineStateCategory.WAITING]: {
+ color: 'warning',
+ icon: '⏸️',
+ label: 'Waiting',
+ description: 'Waiting for user or machine action'
+ },
+ [MachineStateCategory.COMPLETE]: {
+ color: 'success',
+ icon: '✅',
+ label: 'Complete',
+ description: 'Operation completed successfully'
+ },
+ [MachineStateCategory.INTERRUPTED]: {
+ color: 'danger',
+ icon: '⏹️',
+ label: 'Interrupted',
+ description: 'Operation paused or stopped'
+ },
+ [MachineStateCategory.ERROR]: {
+ color: 'danger',
+ icon: '❌',
+ label: 'Error',
+ description: 'Machine in error or unknown state'
+ }
+ };
+
+ return visualMap[category];
+}
diff --git a/src/utils/penParser.ts b/src/utils/penParser.ts
new file mode 100644
index 0000000..9c112e4
--- /dev/null
+++ b/src/utils/penParser.ts
@@ -0,0 +1,128 @@
+import type { PenData, PenStitch, PenColorBlock } from '../types/machine';
+
+// PEN format flags
+const PEN_FEED_DATA = 0x01; // Y-coordinate low byte, bit 0
+const PEN_COLOR_END = 0x03; // X-coordinate low byte, bits 0-2
+const PEN_DATA_END = 0x05; // X-coordinate low byte, bits 0-2
+
+export function parsePenData(data: Uint8Array): PenData {
+ if (data.length < 4 || data.length % 4 !== 0) {
+ throw new Error(`Invalid PEN data size: ${data.length} bytes`);
+ }
+
+ const stitches: PenStitch[] = [];
+ const colorBlocks: PenColorBlock[] = [];
+ const stitchCount = data.length / 4;
+
+ let currentColorStart = 0;
+ let currentColor = 0;
+ let minX = Infinity, maxX = -Infinity;
+ let minY = Infinity, maxY = -Infinity;
+
+ console.log(`Parsing PEN data: ${data.length} bytes, ${stitchCount} stitches`);
+
+ for (let i = 0; i < stitchCount; i++) {
+ const offset = i * 4;
+
+ // Extract coordinates (shifted left by 3 bits in PEN format)
+ const xRaw = data[offset] | (data[offset + 1] << 8);
+ const yRaw = data[offset + 2] | (data[offset + 3] << 8);
+
+ // Extract flags from low 3 bits
+ const xFlags = data[offset] & 0x07;
+ const yFlags = data[offset + 2] & 0x07;
+
+ // Decode coordinates (shift right by 3 to get actual position)
+ // Using signed 16-bit interpretation
+ let x = (xRaw >> 3);
+ let y = (yRaw >> 3);
+
+ // Convert to signed if needed
+ if (x > 0x7FF) x = x - 0x2000;
+ if (y > 0x7FF) y = y - 0x2000;
+
+ const stitch: PenStitch = {
+ x,
+ y,
+ flags: (xFlags & 0x07) | (yFlags & 0x07),
+ isJump: (yFlags & PEN_FEED_DATA) !== 0,
+ };
+
+ stitches.push(stitch);
+
+ // Track bounds
+ if (!stitch.isJump) {
+ minX = Math.min(minX, x);
+ maxX = Math.max(maxX, x);
+ minY = Math.min(minY, y);
+ maxY = Math.max(maxY, y);
+ }
+
+ // Check for color change or data end
+ if (xFlags === PEN_COLOR_END) {
+ const block: PenColorBlock = {
+ startStitch: currentColorStart,
+ endStitch: i,
+ colorIndex: currentColor,
+ };
+ colorBlocks.push(block);
+
+ console.log(
+ `Color ${currentColor}: stitches ${currentColorStart}-${i} (${
+ i - currentColorStart + 1
+ } stitches)`
+ );
+
+ currentColor++;
+ currentColorStart = i + 1;
+ } else if (xFlags === PEN_DATA_END) {
+ if (currentColorStart < i) {
+ const block: PenColorBlock = {
+ startStitch: currentColorStart,
+ endStitch: i,
+ colorIndex: currentColor,
+ };
+ colorBlocks.push(block);
+
+ console.log(
+ `Color ${currentColor} (final): stitches ${currentColorStart}-${i} (${
+ i - currentColorStart + 1
+ } stitches)`
+ );
+
+ currentColor++;
+ }
+ console.log(`Data end marker at stitch ${i}`);
+ break;
+ }
+ }
+
+ const result: PenData = {
+ stitches,
+ colorBlocks,
+ totalStitches: stitches.length,
+ colorCount: colorBlocks.length,
+ bounds: {
+ minX: minX === Infinity ? 0 : minX,
+ maxX: maxX === -Infinity ? 0 : maxX,
+ minY: minY === Infinity ? 0 : minY,
+ maxY: maxY === -Infinity ? 0 : maxY,
+ },
+ };
+
+ console.log(
+ `Parsed: ${result.totalStitches} stitches, ${result.colorCount} colors`
+ );
+ console.log(`Bounds: (${result.bounds.minX}, ${result.bounds.minY}) to (${result.bounds.maxX}, ${result.bounds.maxY})`);
+
+ return result;
+}
+
+export function getStitchColor(penData: PenData, stitchIndex: number): number {
+ for (const block of penData.colorBlocks) {
+ if (stitchIndex >= block.startStitch && stitchIndex <= block.endStitch) {
+ return block.colorIndex;
+ }
+ }
+ return -1;
+}
diff --git a/src/utils/pyodideLoader.ts b/src/utils/pyodideLoader.ts
new file mode 100644
index 0000000..7bbd519
--- /dev/null
+++ b/src/utils/pyodideLoader.ts
@@ -0,0 +1,90 @@
+import { loadPyodide, type PyodideInterface } from "pyodide";
+
+export type PyodideState = "not_loaded" | "loading" | "ready" | "error";
+
+class PyodideLoader {
+ private pyodide: PyodideInterface | null = null;
+ private state: PyodideState = "not_loaded";
+ private error: string | null = null;
+ private loadPromise: Promise<PyodideInterface> | null = null;
+
+ /**
+ * Get the current Pyodide state
+ */
+ getState(): PyodideState {
+ return this.state;
+ }
+
+ /**
+ * Get the error message if state is 'error'
+ */
+ getError(): string | null {
+ return this.error;
+ }
+
+ /**
+ * Initialize Pyodide and install PyStitch
+ */
+ async initialize(): Promise<PyodideInterface> {
+ // If already ready, return immediately
+ if (this.state === "ready" && this.pyodide) {
+ return this.pyodide;
+ }
+
+ // If currently loading, wait for the existing promise
+ if (this.loadPromise) {
+ return this.loadPromise;
+ }
+
+ // Start loading
+ this.state = "loading";
+ this.error = null;
+
+ this.loadPromise = (async () => {
+ try {
+ console.log("[PyodideLoader] Loading Pyodide...");
+
+ // Load Pyodide with CDN indexURL for packages
+ // The core files will be loaded from our bundle, but packages will come from CDN
+ this.pyodide = await loadPyodide();
+
+ console.log("[PyodideLoader] Pyodide loaded, loading micropip...");
+
+ // Load micropip package
+ /*await this.pyodide.loadPackage('micropip');
+
+ console.log('[PyodideLoader] Installing PyStitch...');
+
+ // Install PyStitch using micropip
+ await this.pyodide.runPythonAsync(`
+ import micropip
+ await micropip.install('pystitch')
+ `);*/
+ await this.pyodide.loadPackage("pystitch-1.0.0-py3-none-any.whl");
+
+ console.log("[PyodideLoader] PyStitch installed successfully");
+
+ this.state = "ready";
+ return this.pyodide;
+ } catch (err) {
+ this.state = "error";
+ this.error =
+ err instanceof Error ? err.message : "Unknown error loading Pyodide";
+ console.error("[PyodideLoader] Error:", this.error);
+ throw err;
+ }
+ })();
+
+ return this.loadPromise;
+ }
+
+ /**
+ * Get the Pyodide instance (must be initialized first)
+ */
+ getInstance(): PyodideInterface | null {
+ return this.pyodide;
+ }
+}
+
+// Export singleton instance
+export const pyodideLoader = new PyodideLoader();
diff --git a/src/utils/pystitchConverter.ts b/src/utils/pystitchConverter.ts
new file mode 100644
index 0000000..4c31af8
--- /dev/null
+++ b/src/utils/pystitchConverter.ts
@@ -0,0 +1,233 @@
+import { pyodideLoader } from './pyodideLoader';
+
+// PEN format flags
+const PEN_FEED_DATA = 0x01; // Y-coordinate low byte, bit 0 (jump)
+const PEN_COLOR_END = 0x03; // X-coordinate low byte, bits 0-2
+const PEN_DATA_END = 0x05; // X-coordinate low byte, bits 0-2
+
+// Embroidery command constants (from pyembroidery)
+const MOVE = 0x10;
+const COLOR_CHANGE = 0x40;
+const STOP = 0x80;
+const END = 0x100;
+
+export interface PesPatternData {
+ stitches: number[][];
+ threads: Array<{
+ color: number;
+ hex: string;
+ }>;
+ penData: Uint8Array;
+ colorCount: number;
+ stitchCount: number;
+ bounds: {
+ minX: number;
+ maxX: number;
+ minY: number;
+ maxY: number;
+ };
+}
+
+/**
+ * Reads a PES file using PyStitch and converts it to PEN format
+ */
+export async function convertPesToPen(file: File): Promise<PesPatternData> {
+ // Ensure Pyodide is initialized
+ const pyodide = await pyodideLoader.initialize();
+
+ // Read the PES file
+ const buffer = await file.arrayBuffer();
+ const uint8Array = new Uint8Array(buffer);
+
+ // Write file to Pyodide virtual filesystem
+ const filename = '/tmp/pattern.pes';
+ pyodide.FS.writeFile(filename, uint8Array);
+
+ // Read the pattern using PyStitch
+ const result = await pyodide.runPythonAsync(`
+import pystitch
+
+# Read the PES file
+pattern = pystitch.read('${filename}')
+
+# PyStitch groups stitches by color blocks using get_as_stitchblock
+# This returns tuples of (thread, stitches_list) for each color block
+stitches_with_colors = []
+block_index = 0
+
+# Iterate through stitch blocks
+# Each block is a tuple containing (thread, stitch_list)
+for block in pattern.get_as_stitchblock():
+ if isinstance(block, tuple):
+ # Extract thread and stitch list from tuple
+ thread_obj = None
+ stitches_list = None
+
+ for elem in block:
+ # Check if this is the thread object (has color or hex_color attributes)
+ if hasattr(elem, 'color') or hasattr(elem, 'hex_color'):
+ thread_obj = elem
+ # Check if this is the stitch list
+ elif isinstance(elem, list) and len(elem) > 0 and isinstance(elem[0], list):
+ stitches_list = elem
+
+ if stitches_list:
+ # Find the index of this thread in the threadlist
+ thread_index = block_index
+ if thread_obj and hasattr(pattern, 'threadlist'):
+ for i, t in enumerate(pattern.threadlist):
+ if t is thread_obj:
+ thread_index = i
+ break
+
+ for stitch in stitches_list:
+ # stitch is [x, y, command]
+ stitches_with_colors.append([stitch[0], stitch[1], stitch[2], thread_index])
+
+ block_index += 1
+
+# Convert to JSON-serializable format
+{
+ 'stitches': stitches_with_colors,
+ 'threads': [
+ {
+ 'color': thread.color if hasattr(thread, 'color') else 0,
+ 'hex': thread.hex_color() if hasattr(thread, 'hex_color') else '#000000'
+ }
+ for thread in pattern.threadlist
+ ],
+ 'thread_count': len(pattern.threadlist),
+ 'stitch_count': len(stitches_with_colors),
+ 'block_count': block_index
+}
+ `);
+
+ // Convert Python result to JavaScript
+ const data = result.toJs({ dict_converter: Object.fromEntries });
+
+ // Clean up virtual file
+ try {
+ pyodide.FS.unlink(filename);
+ } catch (e) {
+ // Ignore errors
+ }
+
+ // Extract stitches and validate
+ const stitches: number[][] = Array.from(data.stitches).map((stitch: any) =>
+ Array.from(stitch) as number[]
+ );
+
+ if (!stitches || stitches.length === 0) {
+ throw new Error('Invalid PES file or no stitches found');
+ }
+
+ // Extract thread data
+ const threads = data.threads.map((thread: any) => ({
+ color: thread.color || 0,
+ hex: thread.hex || '#000000',
+ }));
+
+ // Track bounds
+ let minX = Infinity;
+ let maxX = -Infinity;
+ let minY = Infinity;
+ let maxY = -Infinity;
+
+ // Convert to PEN format
+ const penStitches: number[] = [];
+ let currentColor = stitches[0]?.[3] ?? 0; // Track current color using stitch color index
+
+ for (let i = 0; i < stitches.length; i++) {
+ const stitch = stitches[i];
+ const x = Math.round(stitch[0]);
+ const y = Math.round(stitch[1]);
+ const cmd = stitch[2];
+ const stitchColor = stitch[3]; // Color index from PyStitch
+
+ // Track bounds for non-jump stitches
+ if ((cmd & MOVE) === 0) {
+ minX = Math.min(minX, x);
+ maxX = Math.max(maxX, x);
+ minY = Math.min(minY, y);
+ maxY = Math.max(maxY, y);
+ }
+
+ // Encode coordinates with flags in low 3 bits
+ // Shift coordinates left by 3 bits to make room for flags
+ let xEncoded = (x << 3) & 0xFFFF;
+ let yEncoded = (y << 3) & 0xFFFF;
+
+ // Add jump flag if this is a move command
+ if ((cmd & MOVE) !== 0) {
+ yEncoded |= PEN_FEED_DATA;
+ }
+
+ // Check for color change by comparing stitch color index
+ // Mark the LAST stitch of the previous color with PEN_COLOR_END
+ const nextStitch = stitches[i + 1];
+ const nextStitchColor = nextStitch?.[3];
+
+ if (nextStitchColor !== undefined && nextStitchColor !== stitchColor) {
+ // This is the last stitch before a color change
+ xEncoded = (xEncoded & 0xFFF8) | PEN_COLOR_END;
+ currentColor = nextStitchColor;
+ }
+
+ // Add stitch as 4 bytes: [X_low, X_high, Y_low, Y_high]
+ penStitches.push(
+ xEncoded & 0xFF,
+ (xEncoded >> 8) & 0xFF,
+ yEncoded & 0xFF,
+ (yEncoded >> 8) & 0xFF
+ );
+
+ // Check for end command
+ if ((cmd & END) !== 0) {
+ // Mark as data end
+ const lastIdx = penStitches.length - 4;
+ penStitches[lastIdx] = (penStitches[lastIdx] & 0xF8) | PEN_DATA_END;
+ break;
+ }
+ }
+
+ // Mark the last stitch with DATA_END if not already marked
+ if (penStitches.length > 0) {
+ const lastIdx = penStitches.length - 4;
+ if ((penStitches[lastIdx] & 0x07) !== PEN_DATA_END) {
+ penStitches[lastIdx] = (penStitches[lastIdx] & 0xF8) | PEN_DATA_END;
+ }
+ }
+
+ const penData = new Uint8Array(penStitches);
+
+ return {
+ stitches,
+ threads,
+ penData,
+ colorCount: data.thread_count,
+ stitchCount: data.stitch_count,
+ bounds: {
+ minX: minX === Infinity ? 0 : minX,
+ maxX: maxX === -Infinity ? 0 : maxX,
+ minY: minY === Infinity ? 0 : minY,
+ maxY: maxY === -Infinity ? 0 : maxY,
+ },
+ };
+}
+
+/**
+ * Get thread color from pattern data
+ */
+export function getThreadColor(data: PesPatternData, colorIndex: number): string {
+ if (!data.threads || colorIndex < 0 || colorIndex >= data.threads.length) {
+ // Default colors if not specified or index out of bounds
+ const defaultColors = [
+ '#FF0000', '#00FF00', '#0000FF', '#FFFF00',
+ '#FF00FF', '#00FFFF', '#FFA500', '#800080',
+ ];
+ const safeIndex = Math.max(0, colorIndex) % defaultColors.length;
+ return defaultColors[safeIndex];
+ }
+
+ return data.threads[colorIndex]?.hex || '#000000';
+}
diff --git a/src/web-bluetooth.d.ts b/src/web-bluetooth.d.ts
new file mode 100644
index 0000000..e9a191c
--- /dev/null
+++ b/src/web-bluetooth.d.ts
@@ -0,0 +1,125 @@
+// WebBluetooth API type declarations
+// https://webbluetoothcg.github.io/web-bluetooth/
+
+interface BluetoothRemoteGATTServer {
+ device: BluetoothDevice;
+ connected: boolean;
+ connect(): Promise<BluetoothRemoteGATTServer>;
+ disconnect(): void;
+ getPrimaryService(service: BluetoothServiceUUID): Promise<BluetoothRemoteGATTService>;
+ getPrimaryServices(service?: BluetoothServiceUUID): Promise<BluetoothRemoteGATTService[]>;
+}
+
+interface BluetoothDevice extends EventTarget {
+ id: string;
+ name?: string;
+ gatt?: BluetoothRemoteGATTServer;
+ watchingAdvertisements: boolean;
+ watchAdvertisements(options?: WatchAdvertisementsOptions): Promise<void>;
+ unwatchAdvertisements(): void;
+ forget(): Promise<void>;
+ addEventListener(
+ type: 'gattserverdisconnected' | 'advertisementreceived',
+ listener: EventListenerOrEventListenerObject,
+ options?: boolean | AddEventListenerOptions
+ ): void;
+}
+
+interface WatchAdvertisementsOptions {
+ signal?: AbortSignal;
+}
+
+interface BluetoothRemoteGATTService extends EventTarget {
+ device: BluetoothDevice;
+ uuid: string;
+ isPrimary: boolean;
+ getCharacteristic(characteristic: BluetoothCharacteristicUUID): Promise<BluetoothRemoteGATTCharacteristic>;
+ getCharacteristics(characteristic?: BluetoothCharacteristicUUID): Promise<BluetoothRemoteGATTCharacteristic[]>;
+ getIncludedService(service: BluetoothServiceUUID): Promise<BluetoothRemoteGATTService>;
+ getIncludedServices(service?: BluetoothServiceUUID): Promise<BluetoothRemoteGATTService[]>;
+}
+
+interface BluetoothRemoteGATTCharacteristic extends EventTarget {
+ service: BluetoothRemoteGATTService;
+ uuid: string;
+ properties: BluetoothCharacteristicProperties;
+ value?: DataView;
+ getDescriptor(descriptor: BluetoothDescriptorUUID): Promise<BluetoothRemoteGATTDescriptor>;
+ getDescriptors(descriptor?: BluetoothDescriptorUUID): Promise<BluetoothRemoteGATTDescriptor[]>;
+ readValue(): Promise<DataView>;
+ writeValue(value: BufferSource): Promise<void>;
+ writeValueWithResponse(value: BufferSource): Promise<void>;
+ writeValueWithoutResponse(value: BufferSource): Promise<void>;
+ startNotifications(): Promise<BluetoothRemoteGATTCharacteristic>;
+ stopNotifications(): Promise<BluetoothRemoteGATTCharacteristic>;
+ addEventListener(
+ type: 'characteristicvaluechanged',
+ listener: EventListenerOrEventListenerObject,
+ options?: boolean | AddEventListenerOptions
+ ): void;
+}
+
+interface BluetoothCharacteristicProperties {
+ broadcast: boolean;
+ read: boolean;
+ writeWithoutResponse: boolean;
+ write: boolean;
+ notify: boolean;
+ indicate: boolean;
+ authenticatedSignedWrites: boolean;
+ reliableWrite: boolean;
+ writableAuxiliaries: boolean;
+}
+
+interface BluetoothRemoteGATTDescriptor {
+ characteristic: BluetoothRemoteGATTCharacteristic;
+ uuid: string;
+ value?: DataView;
+ readValue(): Promise<DataView>;
+ writeValue(value: BufferSource): Promise<void>;
+}
+
+interface RequestDeviceOptions {
+ filters?: BluetoothLEScanFilter[];
+ optionalServices?: BluetoothServiceUUID[];
+ acceptAllDevices?: boolean;
+}
+
+interface BluetoothLEScanFilter {
+ services?: BluetoothServiceUUID[];
+ name?: string;
+ namePrefix?: string;
+ manufacturerData?: BluetoothManufacturerDataFilter[];
+ serviceData?: BluetoothServiceDataFilter[];
+}
+
+interface BluetoothManufacturerDataFilter {
+ companyIdentifier: number;
+ dataPrefix?: BufferSource;
+ mask?: BufferSource;
+}
+
+interface BluetoothServiceDataFilter {
+ service: BluetoothServiceUUID;
+ dataPrefix?: BufferSource;
+ mask?: BufferSource;
+}
+
+type BluetoothServiceUUID = number | string;
+type BluetoothCharacteristicUUID = number | string;
+type BluetoothDescriptorUUID = number | string;
+
+interface Bluetooth extends EventTarget {
+ getAvailability(): Promise<boolean>;
+ requestDevice(options?: RequestDeviceOptions): Promise<BluetoothDevice>;
+ getDevices(): Promise<BluetoothDevice[]>;
+ addEventListener(
+ type: 'availabilitychanged',
+ listener: EventListenerOrEventListenerObject,
+ options?: boolean | AddEventListenerOptions
+ ): void;
+}
+
+interface Navigator {
+ bluetooth: Bluetooth;
+}
diff --git a/tsconfig.app.json b/tsconfig.app.json
new file mode 100644
index 0000000..a9b5a59
--- /dev/null
+++ b/tsconfig.app.json
@@ -0,0 +1,28 @@
+{
+ "compilerOptions": {
+ "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
+ "target": "ES2022",
+ "useDefineForClassFields": true,
+ "lib": ["ES2022", "DOM", "DOM.Iterable"],
+ "module": "ESNext",
+ "types": ["vite/client"],
+ "skipLibCheck": true,
+
+ /* Bundler mode */
+ "moduleResolution": "bundler",
+ "allowImportingTsExtensions": true,
+ "verbatimModuleSyntax": true,
+ "moduleDetection": "force",
+ "noEmit": true,
+ "jsx": "react-jsx",
+
+ /* Linting */
+ "strict": true,
+ "noUnusedLocals": true,
+ "noUnusedParameters": true,
+ "erasableSyntaxOnly": true,
+ "noFallthroughCasesInSwitch": true,
+ "noUncheckedSideEffectImports": true
+ },
+ "include": ["src"]
+}
diff --git a/tsconfig.json b/tsconfig.json
new file mode 100644
index 0000000..1ffef60
--- /dev/null
+++ b/tsconfig.json
@@ -0,0 +1,7 @@
+{
+ "files": [],
+ "references": [
+ { "path": "./tsconfig.app.json" },
+ { "path": "./tsconfig.node.json" }
+ ]
+}
diff --git a/tsconfig.node.json b/tsconfig.node.json
new file mode 100644
index 0000000..8a67f62
--- /dev/null
+++ b/tsconfig.node.json
@@ -0,0 +1,26 @@
+{
+ "compilerOptions": {
+ "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo",
+ "target": "ES2023",
+ "lib": ["ES2023"],
+ "module": "ESNext",
+ "types": ["node"],
+ "skipLibCheck": true,
+
+ /* Bundler mode */
+ "moduleResolution": "bundler",
+ "allowImportingTsExtensions": true,
+ "verbatimModuleSyntax": true,
+ "moduleDetection": "force",
+ "noEmit": true,
+
+ /* Linting */
+ "strict": true,
+ "noUnusedLocals": true,
+ "noUnusedParameters": true,
+ "erasableSyntaxOnly": true,
+ "noFallthroughCasesInSwitch": true,
+ "noUncheckedSideEffectImports": true
+ },
+ "include": ["vite.config.ts"]
+}
diff --git a/vite.config.ts b/vite.config.ts
new file mode 100644
index 0000000..8fb08db
--- /dev/null
+++ b/vite.config.ts
@@ -0,0 +1,37 @@
+import { defineConfig } from 'vite'
+import react from '@vitejs/plugin-react'
+import { viteStaticCopy } from 'vite-plugin-static-copy'
+import { dirname, join } from 'path'
+import { fileURLToPath } from 'url'
+
+const PYODIDE_EXCLUDE = [
+ '!**/*.{md,html}',
+ '!**/*.d.ts',
+ '!**/node_modules',
+]
+
+function viteStaticCopyPyodide() {
+ const pyodideDir = dirname(fileURLToPath(import.meta.resolve('pyodide')))
+ return viteStaticCopy({
+ targets: [
+ {
+ src: [join(pyodideDir, '*').replace(/\\/g, '/')].concat(PYODIDE_EXCLUDE),
+ dest: 'assets',
+ },
+ ],
+ })
+}
+
+// https://vite.dev/config/
+export default defineConfig({
+ plugins: [react(), viteStaticCopyPyodide()],
+ optimizeDeps: {
+ exclude: ['pyodide'],
+ },
+ server: {
+ headers: {
+ 'Cross-Origin-Opener-Policy': 'same-origin',
+ 'Cross-Origin-Embedder-Policy': 'require-corp',
+ },
+ },
+})
Served by rngit 1.3.3 - Generated in 0.12s